diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9247b719 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,10 @@ +[env] +# To use built-in math functions, this compile time flag must be set +# See https://www.sqlite.org/draft/lang_mathfunc.html as a reference +# According to Cargo docs this will not overwrite any env var that was already +# set by the user, and this is a good thing. If the user already set some +# LIBSQLITE3_FLAGS, he probably knows what he is doing. +LIBSQLITE3_FLAGS = "-DSQLITE_ENABLE_MATH_FUNCTIONS" + +[build] +rustflags = [] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..f9dd7f29 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: [lovasoa] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f407052..8c11db3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,41 +25,70 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: npm ci + - run: npm test - name: Set up cargo cache - uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 + with: + shared-key: rust-sqlpage-proj-test + save-if: ${{ github.ref == 'refs/heads/main' }} - run: cargo fmt --all -- --check - - run: cargo clippy - - run: cargo test --all-features - - run: cargo test + - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo test --features odbc-static + - name: Upload Linux binary + uses: actions/upload-artifact@v4 + with: + name: sqlpage-linux-debug + path: "target/debug/sqlpage" test: - needs: compile_and_lint runs-on: ubuntu-latest strategy: matrix: - database: ["postgres", "mysql", "mssql"] + include: + - database: postgres + container: postgres + db_url: "postgres://root:Password123!@127.0.0.1/sqlpage" + - database: mysql + container: mysql + db_url: "mysql://root:Password123!@127.0.0.1/sqlpage" + - database: mssql + container: mssql + db_url: "mssql://root:Password123!@127.0.0.1/sqlpage" + - database: odbc + container: postgres + db_url: "Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!" + setup_odbc: true steps: - uses: actions/checkout@v4 - name: Set up cargo cache - uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 + with: + shared-key: rust-sqlpage-proj-test + save-if: false + - name: Install PostgreSQL ODBC driver + if: matrix.setup_odbc + run: sudo apt-get install -y odbc-postgresql - name: Start database container - run: docker compose up --wait ${{ matrix.database }} + run: docker compose up --wait ${{ matrix.container }} - name: Show container logs if: failure() - run: docker compose logs ${{ matrix.database }} + run: docker compose logs ${{ matrix.container }} - name: Run tests against ${{ matrix.database }} - run: cargo test + timeout-minutes: 5 + run: cargo test --features odbc-static env: - DATABASE_URL: ${{ matrix.database }}://root:Password123!@127.0.0.1/sqlpage + DATABASE_URL: ${{ matrix.db_url }} RUST_BACKTRACE: 1 - RUST_LOG: sqlpage=debug + MALLOC_CHECK_: 3 + MALLOC_PERTURB_: 10 windows_test: runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Set up cargo cache - uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - name: Check port usage run: netstat -bano - run: cargo test @@ -67,7 +96,7 @@ jobs: RUST_BACKTRACE: 1 - name: Upload Windows binary uses: actions/upload-artifact@v4 - with: + with: name: sqlpage-windows-debug path: "target/debug/sqlpage.exe" diff --git a/.github/workflows/official-site.yml b/.github/workflows/official-site.yml index 0ec66068..3ad48ef3 100644 --- a/.github/workflows/official-site.yml +++ b/.github/workflows/official-site.yml @@ -8,7 +8,7 @@ on: concurrency: site-deploy jobs: - deploy: + deploy_official_site: runs-on: ubuntu-latest steps: - name: Cloning repo diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 46235b79..7865ab24 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -14,16 +14,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up cargo cache - uses: Swatinem/rust-cache@378c8285a4eaf12899d11bea686a763e906956af + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 - uses: actions/setup-node@v4 with: node-version: lts/* cache: 'npm' cache-dependency-path: ./tests/end-to-end/package-lock.json - - name: Install dependencies - run: | - npm ci - npx playwright install --with-deps chromium + - run: sudo apt-get update && sudo apt-get install -y unixodbc-dev + - run: npm ci && npx playwright install --with-deps chromium - name: build sqlpage run: cargo build working-directory: ./examples/official-site diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24294c95..e32cf37e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,34 +4,101 @@ on: # Sequence of patterns matched against refs/tags tags: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + branches: + - "release-test" name: Create Release +permissions: + contents: write + actions: read + jobs: - build: - name: Build sqlpage binaries + build-macos-windows: + name: Build sqlpage binaries (macOS & Windows) runs-on: ${{ matrix.os }} strategy: matrix: - # Use an older ubuntu to compile with an older glibc - os: [macos-latest, windows-latest, ubuntu-20.04] + os: [macos-latest, windows-latest] include: - os: windows-latest - binary_path: target/superoptimized/sqlpage.exe + binary_extension: .exe + target: x86_64-pc-windows-msvc + features: "" - os: macos-latest - binary_path: target/superoptimized/sqlpage - - os: ubuntu-20.04 - binary_path: target/superoptimized/sqlpage + target: x86_64-apple-darwin + features: "odbc-static" steps: - - run: rustup toolchain install stable --profile minimal - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} - name: Set up cargo cache - uses: Swatinem/rust-cache@dd05243424bd5c0e585e4b55eb2d7615cdd32f1f - - run: cargo build --profile superoptimized --locked - - uses: actions/upload-artifact@v4 + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 + - name: Build + run: cargo build --profile superoptimized --locked --target ${{ matrix.target }} --features "${{ matrix.features }}" + - name: Upload unsigned Windows artifact + if: matrix.os == 'windows-latest' + id: upload_unsigned + uses: actions/upload-artifact@v4 + with: + name: unsigned-windows + path: target/${{ matrix.target }}/superoptimized/sqlpage.exe + if-no-files-found: error + + - name: Submit signing request to SignPath + if: matrix.os == 'windows-latest' + id: signpath + uses: signpath/github-action-submit-signing-request@v1.1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: '45fd8443-c7ca-4d29-a68b-608948185335' + project-slug: 'sqlpage' + signing-policy-slug: 'release-signing' + github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: './signed-windows' + wait-for-completion-timeout-in-seconds: 7200 + service-unavailable-timeout-in-seconds: 1800 + download-signed-artifact-timeout-in-seconds: 1800 + + - name: Upload signed Windows artifact + if: matrix.os == 'windows-latest' + uses: actions/upload-artifact@v4 + with: + name: sqlpage windows-latest + path: signed-windows/sqlpage.exe + if-no-files-found: error + + - name: Upload artifact (non-Windows) + if: matrix.os != 'windows-latest' + uses: actions/upload-artifact@v4 with: name: sqlpage ${{ matrix.os }} - path: ${{ matrix.binary_path }} + path: target/${{ matrix.target }}/superoptimized/sqlpage${{ matrix.binary_extension }} + if-no-files-found: error + + build-linux: + name: Build sqlpage binaries (Linux) + runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_28_x86_64 + steps: + - uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu + - name: Set up cargo cache + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 + - name: Build + run: cargo build --profile superoptimized --locked --target x86_64-unknown-linux-gnu --features "odbc-static" + - uses: actions/upload-artifact@v4 + with: + name: sqlpage ubuntu-latest + path: target/x86_64-unknown-linux-gnu/superoptimized/sqlpage + if-no-files-found: error + build-aws: name: Build AWS Lambda Serverless zip image runs-on: ubuntu-latest @@ -43,10 +110,12 @@ jobs: with: name: sqlpage aws lambda serverless image path: sqlpage-aws-lambda.zip + create_release: name: Create Github Release - needs: [build, build-aws] + needs: [build-macos-windows, build-linux, build-aws] runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -55,13 +124,13 @@ jobs: chmod +x sqlpage*/sqlpage; mv 'sqlpage macos-latest/sqlpage' sqlpage.bin; tar --create --file sqlpage-macos.tgz --gzip sqlpage.bin sqlpage/sqlpage.json sqlpage/migrations sqlpage/templates sqlpage/sqlpage.json; - mv 'sqlpage ubuntu-20.04/sqlpage' sqlpage.bin; + mv 'sqlpage ubuntu-latest/sqlpage' sqlpage.bin; tar --create --file sqlpage-linux.tgz --gzip sqlpage.bin sqlpage/migrations sqlpage/templates sqlpage/sqlpage.json; mv 'sqlpage windows-latest/sqlpage.exe' . zip -r sqlpage-windows.zip sqlpage.exe sqlpage/migrations sqlpage/templates sqlpage/sqlpage.json; - name: Create Release id: create_release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }} @@ -72,12 +141,15 @@ jobs: sqlpage-linux.tgz sqlpage-macos.tgz sqlpage aws lambda serverless image/sqlpage-aws-lambda.zip + cargo_publish: name: Publish to crates.io runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v4 - name: Set up cargo cache - uses: Swatinem/rust-cache@dd05243424bd5c0e585e4b55eb2d7615cdd32f1f + uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 + - run: sudo apt-get update && sudo apt-get install -y unixodbc-dev - name: Publish to crates.io run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} diff --git a/.gitignore b/.gitignore index abb6335c..9cf4a906 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_STORE /target sqlpage.db .idea/ @@ -6,4 +7,7 @@ docs/presentation-pgconf.html examples/inrap_badass/ sqlpage/https/* x.sql +xbed.sql **/sqlpage.bin +node_modules/ +sqlpage/sqlpage.db diff --git a/.vscode/launch.json b/.vscode/launch.json index 3a101da1..74ef38df 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,83 +1,64 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'sqlpage'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=sqlpage" - ], - "filter": { - "name": "sqlpage", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'sqlpage'", - "cargo": { - "args": [ - "build", - "--bin=sqlpage", - "--package=sqlpage" - ], - "filter": { - "name": "sqlpage", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'sqlpage'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=sqlpage", - "--package=sqlpage" - ], - "filter": { - "name": "sqlpage", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug integration test 'index'", - "cargo": { - "args": [ - "test", - "--no-run", - "--test=index", - "--package=sqlpage" - ], - "filter": { - "name": "index", - "kind": "test" - } - }, - "args": [], - "cwd": "${workspaceFolder}" + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'sqlpage'", + "cargo": { + "args": ["test", "--no-run", "--lib", "--package=sqlpage"], + "filter": { + "name": "sqlpage", + "kind": "lib" } - ] -} \ No newline at end of file + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'sqlpage'", + "cargo": { + "args": ["build", "--bin=sqlpage", "--package=sqlpage"], + "filter": { + "name": "sqlpage", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'sqlpage'", + "cargo": { + "args": ["test", "--no-run", "--bin=sqlpage", "--package=sqlpage"], + "filter": { + "name": "sqlpage", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug integration test 'index'", + "cargo": { + "args": ["test", "--no-run", "--test=index", "--package=sqlpage"], + "filter": { + "name": "index", + "kind": "test" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 352a6265..0ee06669 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,3 @@ { - "rust-analyzer.linkedProjects": [ - "./Cargo.toml" - ] -} \ No newline at end of file + "rust-analyzer.linkedProjects": ["./Cargo.toml"] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ababedd5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +Core Concept: User writes .sql files, SQLPage executes queries, results mapped to handlebars UI components, +HTML streamed to client + +## Validation + +### When working on rust code +Mandatory formatting (rust): `cargo fmt --all` +Mandatory linting: `cargo clippy --all-targets --all-features -- -D warnings` + +### When working on css or js +Frontend formatting: `npm run format` + +More about testing: see [github actions](./.github/workflows/ci.yml). +Project structure: see [contribution guide](./CONTRIBUTING.md) + +NEVER reformat/lint/touch files unrelated to your task. Always run tests/lints/format before stopping when you changed code. + +### Testing + +``` +cargo test # tests with inmemory sqlite by default +``` + +For other databases, see [docker testing setup](./docker-compose.yml) + +``` +docker compose up -d mssql # or postgres or mysql +DATABASE_URL='mssql://root:Password123!@localhost/sqlpage' cargo test # all dbms use the same user:pass and db name +``` + +### Documentation + +Components and functions are documented in [official website](./examples/official-site/sqlpage/migrations/); one migration per component and per function. You CAN update existing migrations, the official site database is recreated from scratch on each deployment. + +official documentation website sql tables: + - `component(name,description,icon,introduced_in_version)` -- icon name from tabler icon + - `parameter(top_level BOOLEAN, name, component REFERENCES component(name), description, description_md, type, optional BOOLEAN)` parameter types: BOOLEAN, COLOR, HTML, ICON, INTEGER, JSON, REAL, TEXT, TIMESTAMP, URL + - `example(component REFERENCES component(name), description, properties JSON)` + +#### Project Conventions + +- Components: defined in `./sqlpage/templates/*.handlebars` +- Functions: `src/webserver/database/sqlpage_functions/functions.rs` registered with `make_function!`. +- [Configuration](./configuration.md): see [AppConfig](./src/app_config.rs) +- Routing: file-based in `src/webserver/routing.rs`; not found handled via `src/default_404.sql`. +- Follow patterns from similar modules before introducing new abstractions. +- frontend: see [css](./sqlpage/sqlpage.css) and [js](./sqlpage/sqlpage.js) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5f0f3e..067dfe10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,811 @@ # CHANGELOG.md -## 0.27.0 (unreleased) +## unreleased + - fix: `sqlpage.variables()` now does not return json objects with duplicate keys when post, get and set variables of the same name are present. The semantics of the returned values remains the same (precedence: set > post > get). + +## 0.41.0 (2025-12-28) + - **New Function**: `sqlpage.oidc_logout_url(redirect_uri)` - Generates a secure logout URL for OIDC-authenticated users with support for [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout) + - Fix compatibility with Auth0 for OpenID-Connect authentification. See https://github.com/ramosbugs/openidconnect-rs/issues/23 + - updated sql parser: https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.60.0.md + - updated apexcharts to 5.3.6: + - https://github.com/apexcharts/apexcharts.js/compare/v5.3.0...v5.3.6 + - https://github.com/apexcharts/apexcharts.js/releases/tag/v5.3.6 + - re-add the `lime` color option to charts + - update default chart color palette; use [Open Colors](https://yeun.github.io/open-color/) + - image + - re-enable text drop shadow in chart data labels + +## 0.40.0 (2025-11-28) + - OIDC login redirects now use HTTP 303 responses so POST submissions are converted to safe GET requests before reaching the identity provider, fixing incorrect reuse of the original POST (HTTP 307) that could break standard auth flows. + - SQLPage now respects [HTTP accept headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) for JSON. You can now easily process the contents of any existing sql page programmatically with: + - `curl -H "Accept: application/json" http://example.com/page.sql`: returns a json array + - `curl -H "Accept: application/x-ndjson" http://example.com/page.sql`: returns one json object per line. + - Fixed a bug in `sqlpage.link`: a link with no path (link to the current page) and no url parameter now works as expected. It used to keep the existing url parameters instead of removing them. `sqlpage.link('', '{}')` now returns `'?'` instead of the empty string. + - `sqlpage.fetch(null)` and `sqlpage.fetch_with_meta(null)` now return `null` instead of throwing an error. + - **New Function**: `sqlpage.set_variable(name, value)` + - Returns a URL with the specified variable set to the given value, preserving other existing variables. + - This is a shorthand for `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables('get'), json_object(name, value)))`. + - **Variable System Improvements**: URL and POST parameters are now immutable, preventing accidental modification. User-defined variables created with `SET` remain mutable. + - **BREAKING**: `$variable` no longer accesses POST parameters. Use `:variable` instead. + - **What changed**: Previously, `$x` would return a POST parameter value if no GET parameter named `x` existed. + - **Fix**: Replace `$x` with `:x` when you need to access form field values. + - **Example**: Change `SELECT $username` to `SELECT :username` when reading form submissions. + - **BREAKING**: `SET $name` no longer makes GET (URL) parameters inaccessible when a URL parameter with the same name exists. + - **What changed**: `SET $name = 'value'` would previously overwrite the URL parameter `$name`. Now it creates an independent SET variable that shadows the URL parameter. + - **Fix**: This is generally the desired behavior. If you need to access the original URL parameter after setting a variable with the same name, extract it from the JSON returned by `sqlpage.variables('get')`. + - **Example**: If your URL is `page.sql?name=john`, and you do `SET $name = 'modified'`, then: + - `$name` will be `'modified'` (the SET variable) + - The original URL parameter is still preserved and accessible: + - `sqlpage.variables('get')->>'name'` returns `'john'` + - **New behavior**: Variable lookup now follows this precedence: + - `$variable` checks SET variables first, then URL parameters + - SET variables always shadow URL/POST parameters with the same name + - **New sqlpage.variables() filters**: + - `sqlpage.variables('get')` returns only URL parameters as JSON + - `sqlpage.variables('post')` returns only POST parameters as JSON + - `sqlpage.variables('set')` returns only user-defined SET variables as JSON + - `sqlpage.variables()` returns all variables merged together, with SET variables taking precedence + - **Deprecation warnings**: Using `$var` when both a URL parameter and POST parameter exist with the same name now shows a warning. In a future version, you'll need to explicitly choose between `$var` (URL) and `:var` (POST). + - Improved performance of `sqlpage.run_sql`. + - On a simple test that just runs 4 run_sql calls, the new version is about 2.7x faster (15,708 req/s vs 5,782 req/s) with lower latency (0.637 ms vs 1.730 ms per request). + - add support for postgres range types + +## v0.39.1 (2025-11-08) + - More precise server timing tracking to debug performance issues + - Fix missing server timing header in some cases + - Implement nice error messages for some header-related errors such as invalid header values. + - `compress_responses` is now set to `false` by default in the configuration. + - When response compression is enabled, additional buffering is needed. Users reported a better experience with pages that load more progressively, reducing the time before the pages' shell is rendered. + - When SQLPage is deployed behind a reverse proxy, compressing responses between sqlpage and the proxy is wasteful. + - In the table component, allow simple objects in custom_actions instead of requiring arrays of objects. + - Fatser icon loading. Previously, even a page containing a single icon required downloading and parsing a ~2MB file. This resulted in a delay where pages initially appeared with a blank space before icons appeared. Icons are now inlined inside pages and appear instantaneously. + - Updated tabler icons to 3.35 + - Fix inaccurate ODBC warnings + - Added support for Microsoft SQL Server named instances: `mssql://user:pass@localhost/db?instance_name=xxx` + - Added a detailed [performance guide](https://sql-page.com/blog?post=Performance+Guide) to the docs. + +## v0.39.0 (2025-10-28) + - Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml + - Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced. + - The shell with a vertical sidebar can now have "active" elements, just like the horizontal header bar. + - New `edit_url`, `delete_url`, and `custom_actions` properties in the [table](https://sql-page.com/component.sql?component=table) component to easily add nice icon buttons to a table. + - SQLPage now sets the [`Server-Timing` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing) in development. So when you have a page that loads slowly, you can open your browser's network inspector, click on the slow request, then open the timing tab to understand where it's spending its time. + - image + - Fixed a memory corruption issue in the builtin odbc driver manager + - ODBC: fix using globally installed system drivers by their name in debian-based linux distributions. + - New [login](https://sql-page.com/component.sql?component=table) component. + + +## v0.38.0 + - Added support for the Open Database Connectivity (ODBC) standard. + - This makes SQLPage compatible with many new databases, including: + - [*ClickHouse*](https://github.com/ClickHouse/clickhouse-odbc), + - [*MongoDB*](https://www.mongodb.com/docs/atlas/data-federation/query/sql/drivers/odbc/connect), + - [*DuckDB*](https://duckdb.org/docs/stable/clients/odbc/overview.html), and through it [many other data sources](https://duckdb.org/docs/stable/data/data_sources), + - [*Oracle*](https://www.oracle.com/database/technologies/releasenote-odbc-ic.html), + - [*Snowflake*](https://docs.snowflake.com/en/developer-guide/odbc/odbc), + - [*BigQuery*](https://cloud.google.com/bigquery/docs/reference/odbc-jdbc-drivers), + - [*IBM DB2*](https://www.ibm.com/support/pages/db2-odbc-cli-driver-download-and-installation-information), + - [*Trino*](https://docs.starburst.io/clients/odbc/odbc-v2.html), and through it [many other data sources](https://trino.io/docs/current/connector.html) + - Added a new `sqlpage.hmac()` function for cryptographic HMAC (Hash-based Message Authentication Code) operations. + - Create and verify secure signatures for webhooks (Shopify, Stripe, GitHub, etc.) + - Generate tamper-proof tokens for API authentication + - Secure download links and temporary access codes + - Supports SHA-256 (default) and SHA-512 algorithms + - Output formats: hexadecimal (default) or base64 (e.g., `sha256-base64`) + - See the [function documentation](https://sql-page.com/functions.sql?function=hmac) for detailed examples + - Fixed a slight spacing issue in the list components empty value display. + - Improved performance of setting a variable to a literal value. `SET x = 'hello'` is now executed locally by SQLPage and does not send anything to the database. This completely removes the cost of extracting static values into variables for cleaner SQL files. + - Enable arbitrary precision in the internal representation of numbers. This guarantees zero precision loss when the database returns very large or very small DECIMAL or NUMERIC values. + +## v0.37.1 + - fixed decoding of UUID values + - Fixed handling of NULL values in `sqlpage.link`. They were encoded as the string `'null'` instead of being omitted from the link's parameters. + - Enable submenu autoclosing (on click) in the shell. This is not ideal, but this prevents a bug introduced in v0.36.0 where the page would scroll back to the top when clicking anywhere on the page after navigating from a submenu. The next version will fix this properly. See https://github.com/sqlpage/SQLPage/issues/1011 + - Adopt the new nice visual errors introduced in v0.37.1 for "403 Forbidden" and "429 Too Many Requests" errors. + - Fix a bug in oidc login flows. When two tabs in the same browser initiated a login at the same time, an infinite redirect loop could be triggered. This mainly occured when restoring open tabs after a period of inactivity, often in mobile browsers. + - Multiple small sql parser improvements. + - Adds support for MERGE queries inside CTEs, and MERGE queries with a RETURNING clause. + - https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.59.0.md + +## v0.37.0 + - We now cryptographically sign the Windows app during releases, which proves the file hasn’t been tampered with. Once the production certificate is active, Windows will show a "verified publisher" and should stop showing screens saying "This app might harm your device", "Windows protected your PC" or "Are you sure you want to run this application ?". + - Thanks to https://signpath.io for providing us with a windows signing certificate ! + - Added a new parameter `encoding` to the [fetch](https://sql-page.com/functions.sql?function=fetch) function: + - All [standard web encodings](https://encoding.spec.whatwg.org/#concept-encoding-get) are supported. + - Additionally, `base64` can be specified to decode binary data as base64 (compatible with [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)) + - By default, the old behavior of the `fetch_with_meta` function is preserved: the response body is decoded as `utf-8` if possible, otherwise the response is encoded in `base64`. + - Added a specific warning when a URL parameter and a form field have the same name. The previous general warning about referencing form fields with the `$var` syntax was confusing in that case. + - [modal](https://sql-page.com/component.sql?component=modal) component: allow opening modals with a simple link. + - This allows you to trigger modals from any other component, including tables, maps, forms, lists and more. + - Since modals have their own url inside the page, you can now link to a modal from another page, and if you refresh a page while the modal is open, the modal will stay open. + - modals now have an `open` parameter to open the modal automatically when the page is loaded. + - New [download](https://sql-page.com/component.sql?component=download) component to let the user download files. The files may be stored as BLOBs in the database, local files on the server, or may be fetched from a different server. + - **Enhanced BLOB Support**. You can now return binary data (BLOBs) directly to sqlpage, and it will automatically convert them to data URLs. This allows you to use database BLOBs directly wherever a link is expected, including in the new download component. + - supports columns of type `BYTEA` (PostgreSQL), `BLOB` (MySQL, SQLite), `VARBINARY` and `IMAGE` (mssql) + - Automatic detection of common file types based on magic bytes + - This means you can use a BLOB wherever an image url is expected. For instance: + ```sql + select 'list' as component; + select username as title, avatar_blob as image_url + from users; + ``` + - When a sql file is saved with the wrong character encoding (not UTF8), SQLPage now displays a helpful error messages that points to exactly where in the file the problem is. + - More visual error messages: errors that occured before (such as file access issues) used to generate plain text messages that looked scary to non-technical users. All errors are now displayed nicely in the browser. + - The form component now considers numbers and their string representation as equal when comparing the `value` parameter and the values from the `options` parameter in dropdowns. This makes it easier to use variables (which are always strings) in the value parameter in order to preserve a dropdown field value across page reloads. The following is now valid: + - ```sql + select 'form' as component; + select + 'select' as type, + true as create_new, + true as dropdown, + '2' as value, -- passed as text even if the option values are passed as integers + '[{"label": "A", "value": 1}, {"label": "B", "value": 2}]' as options; + ``` + +## v0.36.1 + - Fix regression introduced in v0.36.0: PostgreSQL money values showed as 0.0 + - The recommended way to display money values in postgres is still to format them in the way you expect in SQL. See https://github.com/sqlpage/SQLPage/issues/983 + - updated dependencies + +## v0.36.0 + - added support for the MONEY and SMALLMONEY types in MSSQL. + - include [math functions](https://sqlite.org/lang_mathfunc.html) in the builtin sqlite3 database. + - the sqlpage binary can now help you create new empty migration files from the command line: + ``` + ❯ ./sqlpage create-migration my_new_table + Migration file created: sqlpage/migrations/20250627095944_my_new_table.sql + ``` + - New [modal](https://sql-page.com/component.sql?component=modal) component + - In bar charts: Sort chart categories by name instead of first appearance. This is useful when displaying cumulative bar charts with some series missing data for some x values. + - Updated tabler to v1.4 https://github.com/tabler/tabler/releases/tag/%40tabler%2Fcore%401.4.0 + - Updated tabler-icons to v3.34 (19 new icons) https://tabler.io/changelog#/changelog/tabler-icons-3.34 + - Added support for partially private sites when using OIDC single sign-on: + - The same SQLPage application can now have both publicly accessible and private pages accessible to users authenticated with SSO. + - This allows easily creating a "log in page" that redirects to the OIDC provider. + - See the [configuration](./configuration.md) for `oidc_protected_paths` +- Chart component: accept numerical values passed as strings in pie charts. +- updated sql parser: [v0.57](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.57.0.md) [v0.58](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.58.0.md) + * **Postgres text search types**: allows `tsquery` and `tsvector` data types + ```sql + SELECT 'OpenAI'::text @@ 'open:*'::tsquery; + ``` + * **LIMIT in subqueries**: fixes parsing of `LIMIT` inside subselects + ```sql + SELECT id FROM (SELECT id FROM users ORDER BY id LIMIT 5) AS sub; + ``` + * **MySQL `MEMBER OF`**: JSON array membership test + ```sql + SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]') + ``` + * **Join precedence fix**: corrects interpretation of mixed `JOIN` types without join conditions + ```sql + SELECT * FROM t1 NATURAL JOIN t2 + ``` + * **Unicode identifiers**: allows non‑ASCII names in MySQL/Postgres/SQLite + ```sql + SELECT 用户 AS chinese_name FROM accounts; + ``` + * **Regex and `LIKE` operator fixes**: allow using `~` and `LIKE` with arrays + ```sql + select a ~ any(array['x']); + ``` + * MSSQL output and default keywords in `EXEC` statements + ```sql + EXECUTE dbo.proc1 DEFAULT + ``` +- The file-based routing system was improved. Now, requests to `/xxx` redirect to `/xxx/` only if `/xxx/index.sql` exists. +- fix: When single sign on is enabled, and an anonymous user visits a page with URL parameters, the user is correctly redirected to the page with the parameters after login. +- SQLPage can now read custom claims from Single-Sign-On (SSO) tokens. This allows you to configure your identity provider to include user-specific data, such as roles or permissions, directly in the login token. This information becomes available in your SQL queries, enabling you to build pages that dynamically adapt their content to the authenticated user. +- A bug that caused SSO logins to fail over time has been fixed. The issue occurred because identity providers regularly rotate their security keys, but SQLPage previously only fetched them at startup. The application now automatically refreshes this provider metadata periodically and after login errors, ensuring stable authentication without requiring manual restarts. + +## v0.35.2 + - Fix a bug with zero values being displayed with a non-zero height in stacked bar charts. + - Updated dependencies, including the embedded SQLite database. + - Release binaries are now dynamically linked again, but use GLIBC 2.28 ([released in 2018](https://sourceware.org/glibc/wiki/Glibc%20Timeline)), with is compatible with older linux distributions. + - fixes an issue introduced in 0.35 where custom SQLite extension loading would not work. + - When an user requests a page that does not exist (and the site owner did not provide a custom 404.sql file), we now serve a nice visual 404 web page instead of the ugly textual message and the verbose log messages we used to have. + - ![screenshot 404](https://github.com/user-attachments/assets/02525f9e-91ec-4657-a70f-1b7990cbe25f) + - still returns plain text 404 for non-HTML requests + - Rich text editor: implement a readonly mode, activated when the field is not editable + - [chart](https://sql-page.com/component.sql?component=chart): remove automatic sorting of categories. Values are now displayed in the order they are returned by the query. + +## v0.35.1 + - improve color palette for charts + - Fix some color names not working in the datagrid component + +## v0.35 + - Add support for [single sign-on using OIDC](sql-page.com/sso) + - Allows protecting access to your website using "Sign in with Google/Microsoft/..." + - Fix tooltips not showing on line charts with one or more hidden series + - Update default chart colors and text shadows for better readability with all themes + - Optimize memory layout by boxing large structs. Slightly reduces memory usage. + - New example: [Rich text editor](./examples/rich-text-editor/). Let your users safely write formatted text with links and images. + - Update the Tabler CSS library to [v1.3](https://tabler.io/changelog#/changelog/tabler-1.3). This fixes issues with + - the alignment inside chart tooltips + - the display of lists + - update to [tabler incons v1.33](https://tabler.io/changelog#/changelog/tabler-icons-3.33) with many new icons. + - Add an `active` top-level parameter to the shell component to highlight one of the top bar menu items. Thanks to @andrewsinnovations ! + - Make the [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) customization more flexible, allowing you to harden the default security rules. Thanks to @guspower ! +- Fix vertically truncated text in the list component on empty descriptions. + - ![screenshot](https://github.com/user-attachments/assets/df258e31-6698-4398-8ce5-4d7f396c03ef) + - Updated sqlparser to [v0.56](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.56.0.md), with many improvements including: + - Add support for the xmltable(...) function in postgres + - Add support for MSSQL IF/ELSE statements. + - Added four optional properties to the `big_number` component: + - title_link (string): the URL or path that the Big Number’s title should link to, if any + - title_link_new_tab (bool): how the title link is opened + - value_link (string): the URL or path that the Big Number’s value should link to, if any + - value_link_new_tab (bool): open the link in a new tab + - Add support for nice "switch" checkboxes in the form component using `'switch' as type` + - Add support for headers in the form component using + - Release binaries are statically linked on linux + +## v0.34 (2025-03-23) + +### ✨ Top Features at a Glance +- **Safer deletion flows** in lists +- **Better table styling control** with CSS updates +- **Right-to-Left language support** +- **HTML-enhanced Markdown** in text components +- **Sticky table footers** for better data presentation + +### 🔒 Security First +#### **POST-based Deletions** +List component's `delete_link` now uses secure POST requests: +```sql +SELECT 'list' AS component; +SELECT 'Delete me' AS title, 'delete_item.sql?id=77' AS delete_link; +``` +*Prevents accidental deletions by web crawlers and follows REST best practices* + +#### **Protected Internal Files** +- Files/folders starting with `.` (e.g., `.utils/`) are now inaccessible +- Perfect for internal scripts used with `sqlpage.run_sql()` + +### 🎨 UI & Component Upgrades +#### **Table Styling Revolution** +```css +/* Before: .price | After: */ +._col_price { + background: #f8f9fa; + border-right: 2px solid #dee2e6; +} +``` +- New CSS class pattern: `._col_{column_name}` +- Fixes [#830](https://github.com/sqlpage/SQLPage/issues/830) + +#### **Column component** +```sql +SELECT 'columns' AS component; +SELECT 'View details' AS title; -- No button shown +``` +- Columns without button text now hide empty buttons +- Cleaner interfaces by default + +#### **Sticky Table Footers** +```sql +SELECT + 'table' AS component, + true AS freeze_footers; +SELECT + 'Total' AS label, + SUM(price) AS value, + true AS _sqlpage_footer; +``` +- Keep summary rows visible during scroll +- Use `_sqlpage_footer` on your final data row + +### 🌍 Internationalization +#### **Right-to-Left Support** +```sql +SELECT 'shell' AS component, true AS rtl; +``` +- Enable RTL mode per page via shell component +- Perfect for Arabic, Hebrew, and Persian content + +### 📝 Content Handling +#### **Rich Text Power** +```sql +SELECT 'text' AS component, + '
+ **Important!** + + New *HTML-enhanced* content. +
' + AS unsafe_contents_md; +``` +- New `unsafe_contents_md` allows HTML+Markdown mixing + +#### **Base64 Image Support** +```markdown +![Alt text](data:image/png;base64,iVBORw0KGg...) +``` +- Embed images directly in Markdown fields + +### ⚙️ Configuration Tweaks +```json +{ + "markdown_allow_dangerous_html": false, + "markdown_allow_dangerous_protocol": false +} +``` +- **Markdown safety controls** to change markdown rendering settings + +### 🐛 Notable Fixes +- **SQL Server** + Fixed TINYINT handling crashes +- **Anchor Links** + Corrected display in tables with fixed headers +- **Form Inputs** + Proper handling of `0` values in number fields + +### 💡 Upgrade Guide +1. **CSS Updates** + Search/replace `.your_column` → `._col_your_column` if you have custom css targetting tables. +2. **Deletion Flows** + Test list components using `delete_link`. + You can now add a check that the request method is POST if you want to forbid deletions by simply loading pages. + +[View full configuration options →](./configuration.md) + + +## 0.33.1 (2025-02-25) + +- Fix a bug where the table component would not format numbers if sorting was not enabled. +- Fix a bug with date sorting in the table component. +- Center table descriptions. +- Fix a rare crash on startup in some restricted linux environments. +- Fix a rare but serious issue when on SQLite and MySQL, some variable values were assigned incorrectly + - `CASE WHEN $a THEN $x WHEN $b THEN $y` would be executed as `CASE WHEN $a THEN $b WHEN $x THEN $y` on these databases. + - the issue only occured when using in case expressions where variables were used both in conditions and results. +- Implement parameter deduplication. + Now, when you write `select $x where $x is not null`, the value of `$x` is sent to the database only once. It used to be sent as many times as `$x` appeared in the statement. +- Improve error messages on invalid sqlpage function calls. The messages now contain actionable advice. +- Fix top navigation bar links color. They appeared "muted", with low contrast, since v0.33 +- update to apex charts v4.5.0. This fixes a bug where tick positions in scatter plots would be incorrect. +- New function: `sqlpage.fetch_with_meta` + - This function is similar to `sqlpage.fetch`, but it returns a json object with the following properties: + - `status`: the http status code of the response. + - `headers`: a json object with the response headers. + - `body`: the response body. + - `error`: an error message if the request failed. + - This is useful when interacting with complex or unreliable external APIs. + +## 0.33.0 (2025-02-15) + +### 1. Routing & URL Enhancements 🔀 + +#### **Clean URLs:** +Access your pages without the extra “.sql” suffix. For instance, if your file is `page.sql`, you can now use either: + +| Old URL | New URL | +|---|---| +| `https://example.com/page.sql` | `https://example.com/page` (or `page.sql` still works) | + +Big thanks to [@guspower](https://github.com/guspower) for their contributions! + +#### **Complete Routing Rewrite:** +We overhauled our request routing system for smoother, more predictable routing across every request. + +--- + +### 2. SQLPage Functions ⚙️ + +#### **sqlpage.fetch (Calling External Services)** + +- **HTTP Basic Authentication:** + SQLPage’s `sqlpage.fetch(request)` now supports HTTP Basic Auth. Easily call APIs requiring a username/password. For example: + + ```sql + SET result = sqlpage.fetch(json_object( + 'url', 'https://api.example.com/data', + 'username', 'user', + 'password', 'pass' + )); + ``` + Check out the [[fetch documentation](https://sql-page.com/documentation.sql?component=fetch#component)](https://sql-page.com/documentation.sql?component=fetch#component) for more. + +- **Smarter Fetch Errors & Headers Defaults:** + Get clearer error messages if your HTTP request definition is off (unknown fields, etc.). Plus, if you omit the `headers` parameter, SQLPage now sends a default User‑Agent header that includes the SQLPage version. + +- New Functions: [`sqlpage.request_body`](https://sql-page.com/functions.sql?function=request_body) and [`sqlpage.request_body_base64`](https://sql-page.com/functions.sql?function=request_body_base64) + - Return the raw request body as a string or base64 encoded string. + - Useful to build REST JSON APIs in SQL easily. + - Example: + ```sql + INSERT INTO users (name, email) + VALUES ( + json(sqlpage.request_body())->>'name', + json(sqlpage.request_body())->>'email' + ); + ``` + +- **New Function: [sqlpage.headers](https://sql-page.com/functions.sql?function=headers):** + Easily manage and inspect HTTP headers with the brand‑new [`sqlpage.headers`](https://sql-page.com/functions.sql?function=headers) function. + +### 3. UI Component Enhancements 🎨 + +#### **Table & Card Components** + +- **Table CSS Fixes:** + We fixed a bug where table cells weren’t getting the right CSS classes—your tables now align perfectly. + +- **Native Number Formatting:** + Numeric values in tables are now automatically formatted to your visitor’s locale with proper thousands separators and decimal points, and sorted numerically. + _Example:_ + ![Number Formatting Example](https://github.com/user-attachments/assets/ba51a63f-b9ce-4ab2-a6dd-dfa8e22396de) + +- **Enhanced Card Layouts:** + Customizing your `card` components is now easier: + - The `embed` property auto‑appends the `_sqlpage_embed` parameter for embeddable fragments. + - When rendering an embedded page, the `shell` component is replaced by `shell-empty` to avoid duplicate headers and metadata. + ![Card Layout Example](https://github.com/user-attachments/assets/c5b58402-178a-441e-8966-fd8e341b02bc) + +#### **Form Component Boosts** + +- **Auto‑Submit Forms:** + Set `auto_submit` to true and your form will instantly submit on any field change—ideal for dashboard filters. + *Example:* + ```sql + SELECT 'form' AS component, 'Filter Results' AS title, true AS auto_submit; + SELECT 'date' AS name; + ``` +- **Dynamic Options for Dropdowns:** + Use `options_source` to load dropdown options dynamically from another SQL file. Perfect for autocomplete with large option sets. + *Example:* + ```sql + SELECT 'form' AS component, 'Select Country' AS title, 'countries.sql' AS options_source; + SELECT 'country' AS name; + ``` +- **Markdown in Field Descriptions:** + With the new `description_md` property, render markdown in form field descriptions for improved guidance. +- **Improved Header Error Messages:** + Now you’ll get more helpful errors if header components (e.g., `json`, `cookie`) are used incorrectly. + +--- + +### 4. Chart, Icons & CSS Updates 📊 + +- **ApexCharts Upgrade:** + We updated ApexCharts to [[v4.4.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v4.4.0)](https://github.com/apexcharts/apexcharts.js/releases/tag/v4.4.0) for smoother charts and minor bug fixes. + +- **Tabler Icons & CSS:** + Enjoy a refreshed look: + - Tabler Icons are now [[v3.30.0](https://tabler.io/changelog#/changelog/tabler-icons-3.30)](https://tabler.io/changelog#/changelog/tabler-icons-3.30) with many new icons. + - The CSS framework has been upgraded to [[Tabler 1.0.0](https://github.com/tabler/tabler/releases/tag/v1.0.0)](https://github.com/tabler/tabler/releases/tag/v1.0.0) for improved consistency and a sleeker interface. + +--- + +### 5. CSV Import & Error Handling 📥 + +- **Enhanced CSV Error Messages:** + More descriptive error messages when a CSV import fails (via `copy` and file upload). + +- **Postgres CSV Bug Fix:** + A bug that caused subsequent requests to fail after a CSV import error on PostgreSQL is now fixed. + (See [Issue #788](https://github.com/sqlpage/SQLPage/issues/788) for details.) + +--- + +### 6. SQL Parser & Advanced SQL Support 🔍 + +**Upgraded SQL Parser ([v0.54](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.54.0.md)):** +Our sqlparser is now at [v0.54](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.54.0.md), with support for advanced SQL syntax: + +- **INSERT...SELECT...RETURNING:** + ```sql + INSERT INTO users (name, email) + SELECT :name, :email + WHERE :name IS NOT NULL + RETURNING 'redirect' AS component, 'user.sql?id=' || id AS link; + ``` +- **PostgreSQL’s overlaps operator:** + ```sql + SELECT 'card' AS component, + event_name AS title, + start_time::text || ' - ' || end_time::text AS description + FROM events + WHERE + (start_time, end_time) + OVERLAPS + ($start_filter::timestamp, $end_filter::timestamp); + ``` +- **MySQL’s INSERT...SET syntax:** + ```sql + INSERT INTO users + SET name = :name, email = :email; + ``` + +--- + +## 0.32.1 (2025-01-03) + +This is a bugfix release. + +- Fix a bug where the form component would not display the right checked state in radio buttons and checkboxes. + - https://github.com/sqlpage/SQLPage/issues/751 +- Fix a bug in the [link](https://sql-page.com/component.sql?component=link) component where the properties `view_link`, `edit_link`, and `delete_link` had become incompatible with the main `link` property. +- Updated sqlparser to [v0.53](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.53.0.md) which fixes parse errors when using some advanced SQL syntax + - adds support for SQLite's `UPDATE OR REPLACE` syntax + - adds support for MSSQL's `JSON_ARRAY` and `JSON_OBJECT` functions + - adds support for PostgreSQL's `JSON_OBJECT(key : value)` and `JSON_OBJECT(key VALUE value)` syntax + - fixes the parsing of `true` and `false` in Microsoft SQL Server (mssql): they are now correctly parsed as column names, not as boolean values, since mssql does not support boolean literals. This means you may have to replace `TRUE as some_property` with `1 as some_property` in your SQL code when working with mssql. +- When your SQL contains errors, the error message now displays the precise line(s) number(s) of your file that contain the error. + +## 0.32.0 (2024-12-29) + +- Rollback any open transactions when an error occurs in a SQL file. + - Previously, if an error occurred in the middle of a transaction, the transaction would be left open, and the connection would be returned to the pool. The next request could get a connection with an open half-completed transaction, which could lead to hard to debug issues. + - This allows safely using features that require a transaction, like + - ```sql + BEGIN; + CREATE TEMPORARY TABLE t (x int) ON COMMIT DROP; -- postgres syntax + -- do something with t + -- previously, if an error occurred, the transaction would be left open, and the connection returned to the pool. + -- the next request could get a connection where the table `t` still exists, leading to a new hard to debug error. + COMMIT; + ``` + - This will now automatically rollback the transaction, even if an error occurs in the middle of it. +- Fix a bug where one additional SQL statement was executed after an error occurred in a SQL file. This could cause surprising unexpected behavior. + - ```sql + insert into t values ($invalid_value); -- if this statement fails, ... + insert into t values (42); -- this next statement should not be executed + ``` +- Fix `error returned from database: 1295 (HY000): This command is not supported in the prepared statement protocol yet` when trying to use transactions with MySQL. `START TRANSACTION` now works as expected in MySQL. +- Fix a bug where a multi-select dropdown would unexpectedly open when the form was reset. +- Add a new optional `sqlpage/on_reset.sql` file that can be used to execute some SQL code after the end of each page execution. + - Useful to reset a connection to the database after each request. +- Fix a bug where the `sqlpage.header` function would not work with headers containing uppercase letters. +- Fix a bug where the table component would not sort columns that contained a space in their name. +- Fix a bug where stacked bar charts would not stack the bars correctly in some cases. +- Update ApexCharts to [v4.1.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v4.1.0). +- Temporarily disable automatic tick amount calculation in the chart component. This was causing issues with mislabeled x-axis data, because of a bug in ApexCharts. +- Add a new `max_recursion_depth` configuration option to limit the depth of recursion allowed in the `run_sql` function. +- Fix a bug where the results of the `JSON` function in sqlite would be interpreted as a string instead of a json object. +- Fix a bug where the `sqlpage.environment_variable` function would return an error if the environment variable was not set. Now it returns `null` instead. +- Update ApexCharts to [v4.3.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v4.3.0). +- New `article` property in the text component to display text in a more readable, article-like format. +- Add support for evaluating calls to `coalesce` inside sqlpage functions. This means you can now use `coalesce` inside arguments of sqlpage functions, and it will be evaluated inside sqlpage. For instance, this lets you call `sqlpage.link(coalesce($url, 'https://sql-page.com'))` to create a link that will use the value of `$url` if it is not null, or fallback to `https://sql-page.com` if it is null. +- In the form component, allow the usage of the `value` property in checkboxes and radio buttons. The custom `checked` property still works, but it is now optional. +- Updated the welcome message displayed on the terminal when starting the server to be friendlier and more helpful. +- Display the page footer (by default: `Built with SQLPage`) at the bottom of the page instead of immediately after the main content. +- Improve links in the list component: The entire list item is now clickable, when a `link` property is provided. +- When using the map component without a basemap, use a light background color that respects the theme color. + +## 0.31.0 (2024-11-24) + +### 🚀 **New Features** + +#### **Improved Components** +- [**Columns Component**](https://sql-page.com/component.sql?component=columns) + - Markdown-supported descriptions (`description_md`) allow richer formatting. + - Add simple text items without needing JSON handling. + - Optionally skip displaying items (`null as item`). + - ![columns component screenshot](https://github.com/user-attachments/assets/dd5e1ba7-e12f-4119-a201-0583cf765000) + +- [**Table Component**](https://sql-page.com/component.sql?component=table) + - New **freeze headers and columns** feature improves usability with large tables. + - Enhanced search logic ensures more precise matches (e.g., `"xy"` no longer matches separate `x` and `y` cells in adjacent columns). + - Search box visibility is retained during horizontal scrolling. + *Technical:* Adds `freeze_headers`, `freeze_columns`, and improves the internal search algorithm. + - ![scroll table](https://github.com/user-attachments/assets/546f36fb-b590-487d-8817-47eeed8f1835) + +- [**Form Component**](https://sql-page.com/component.sql?component=form) + - Added an empty option (`empty_option`) to dropdowns, enabling placeholder-like behavior. + - ![form](https://github.com/user-attachments/assets/40a230da-9b1b-49ed-9759-5e21fe812957) + - Improved handling of large form submissions with configurable size limits (`max_uploaded_file_size`, default 5MB). + *Technical:* There used to be a hardcoded limit to 16kB for all forms. +--- + + +#### **Database Enhancements** +- **Support for New Data Types**: + - Microsoft SQL Server now supports `BIT` columns. + - Improved handling of `DATETIMEOFFSET` in MSSQL and `TIMESTAMPTZ` in PostgreSQL, preserving their timezones instead of converting them to UTC. + +- **Better JSON Handling**: + - Accept nested JSON objects and arrays as function parameters. + Useful for advanced usage like calling external APIs using `sqlpage.fetch` with complex data structures. + +- **SQL Parser Update**: + - Upgraded to [v0.52.0](https://github.com/apache/datafusion-sqlparser-rs/blob/main/changelog/0.52.0.md) with new features: + - Added support for: + - advanced `JSON_TABLE` usage in MySQL for working with JSON arrays. + - `EXECUTE` statements with parameters in MSSQL for running stored procedures. + - MSSQL's `TRY_CONVERT` function for type conversion. + - `ANY`, `ALL`, and `SOME` subqueries (e.g., `SELECT * FROM t WHERE a = ANY (SELECT b FROM t2)`). + - `LIMIT max_rows, offset` syntax in SQLite. + - Assigning column names aliases using `=` in MSSQL (e.g., `SELECT col_name = value`). + - Fixes a bug where the parser would fail parse a `SET` clause for a variable named `role`. + +--- + +#### **Security and Performance** +- **Encrypted Login Support for MSSQL**: + - Ensures secure connections with flexible encryption modes: + - No encryption (`?encrypt=not_supported`): For legacy systems and environments where SSL is blocked + - Partial encryption (`?encrypt=off`): Protects login credentials but not data packets. + - Full encryption (`?encrypt=on`): Secures both login and data. + *Technical:* Controlled using the `encrypt` parameter (`not_supported`, `off`, or `strict`) in mssql connection strings. + +- **Chart Library Optimization**: + - Updated ApexCharts to v4.0.0. + - Fixed duplicate library loads, speeding up pages with multiple charts. + - Fixed a bug where [timeline chart tooltips displayed the wrong labels](https://github.com/sqlpage/SQLPage/issues/659). + +--- + +### 🛠 **Bug Fixes** +#### Database and Compatibility Fixes +- **Microsoft SQL Server**: + - Fixed decoding issues for less common data types. + - Resolved bugs in reading `VARCHAR` columns from non-European collations. + - Correctly handles `REAL` values. + +- **SQLite**: + - Eliminated spurious warnings when using SQLPage functions with JSON arguments. + *Technical:* Avoids warnings like `The column _sqlpage_f0_a1 is missing`. + +#### Component Fixes +- **Card Component**: + - Fixed layout issues with embedded content (e.g., removed double borders). + - ![Example Screenshot](https://github.com/user-attachments/assets/ea85438d-5fcb-4eed-b90b-a4385675355d) + - Corrected misaligned loading spinners. + +- **Form Dropdowns**: + - Resolved state retention after form resets, ensuring dropdowns reset correctly. + +#### Usability Enhancements +- Removed unnecessary padding around tables for cleaner layouts. +- Increased spacing between items in the columns component for improved readability. +- Database errors are now consistently logged and displayed with more actionable details. + - ![better errors](https://github.com/user-attachments/assets/f0d2f9ef-9a30-4ff2-af3c-b33a375f2e9b) + *Technical:* Ensures warnings in the browser and console for faster debugging. + +--- + +## 0.30.1 (2024-10-31) +- fix a bug where table sorting would break if table search was not also enabled. + +## 0.30.0 (2024-10-30) + +### 🤖 Easy APIs +- **Enhanced CSV Support**: The [CSV component](https://sql-page.com/component.sql?component=csv) can now create URLs that trigger a CSV download directly on page load. + - This finally makes it possible to allow the download of large datasets as CSV + - This makes it possible to create an API that returns data as CSV and can be easily exposed to other software for interoperabily. + - **Easy [json](https://sql-page.com/component.sql?component=json) APIs** + - The json component now accepts a second sql query, and will return the results as a json array in a very resource-efficient manner. This makes it easier and faster than ever to build REST APIs entirely in SQL. + - ```sql + select 'json' as component; + select * from users; + ``` + - ```json + [ { "id": 0, "name": "Jon Snow" }, { "id": 1, "name": "Tyrion Lannister" } ] + ``` + - **Ease of use** : the component can now be used to automatically format any query result as a json array, without manually using your database''s json functions. + - **server-sent events** : the component can now be used to stream query results to the client in real-time using server-sent events. + +### 🔒 Database Connectivity +- **Encrypted Microsoft SQL Server Connections**: SQLPage now supports encrypted connections to SQL Server databases, enabling connections to secure databases (e.g., those hosted on Azure). +- **Separate Database Password Setting**: Added `database_password` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md) to store passwords securely outside the connection string. This is useful for security purposes, to avoid accidentally leaking the password in logs. This also allows setting the database password as an environment variable directly, without having to URL-encode it inside the connection string. + +### 😎 Developer experience improvements +- **Improved JSON Handling**: SQLPage now automatically converts JSON strings to JSON objects in databases like SQLite and MariaDB, making it easier to use JSON-based components. + - ```sql + -- Now works out of the box in SQLite + select 'big_number' as component; + select 'Daily performance' as title, perf as value; + json_object( + 'label', 'Monthly', + 'link', 'monthly.sql' + ) as dropdown_item + from performance; + ``` + +### 📈 Table & Search Improvements +- **Initial Search Value**: Pre-fill the search bar with a default value in tables with `initial_search_value`, making it easier to set starting filters. +- **Faster Sorting and Searching**: Table filtering and sorting has been entirely rewritten. + - filtering is much faster for large datasets + - sorting columns that contain images and links now works as expected + - Since the new code is smaller, initial page loads should be slightly faster, even on pages that do not use tables + +### 🖼️ UI & UX Improvements + +- **[Carousel](https://sql-page.com/component.sql?component=carousel) Updates**: + - Autoplay works as expected when embedded in a card. + - Set image width and height to prevent layout shifts due to varying image sizes. +- **Improved Site SEO**: The site title in the shell component is no longer in `

` tags, which should aid search engines in understanding content better, and avoid confusing between the site name and the page's title. + +### 🛠️ Fixes and improvements + +- **Shell Component Search**: Fixed search feature when no menu item is defined. +- **Updated Icons**: The Tabler icon set has been refreshed from 3.10 to 3.21, making many new icons available: https://tabler.io/changelog + +## 0.29.0 (2024-09-25) + - New columns component: `columns`. Useful to display a comparison between items, or large key figures to an user. + - ![screenshot](https://github.com/user-attachments/assets/89e4ac34-864c-4427-a926-c38e9bed3f86) + - New foldable component: `foldable`. Useful to display a list of items that can be expanded individually. + - ![screenshot](https://github.com/user-attachments/assets/2274ef5d-7426-46bd-b12c-865c0308a712) + - CLI arguments parsing: SQLPage now processes command-line arguments to set the web root and configuration directory. It also allows getting the currently installed version of SQLPage with `sqlpage --version` without starting the server. + - ``` + $ sqlpage --help + Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components. + + Usage: sqlpage [OPTIONS] + + Options: + -w, --web-root The directory where the .sql files are located + -d, --config-dir The directory where the sqlpage.json configuration, the templates, and the migrations are located + -c, --config-file The path to the configuration file + -h, --help Print help + -V, --version Print version + - Configuration checks: SQLPage now checks if the configuration file is valid when starting the server. This allows to display a helpful error message when the configuration is invalid, instead of crashing or behaving unexpectedly. Notable, we now ensure critical configuration values like directories, timeouts, and connection pool settings are valid. + - ``` + ./sqlpage --web-root /xyz + [ERROR sqlpage] The provided configuration is invalid + Caused by: + Web root is not a valid directory: "/xyz" + - The configuration directory is now created if it does not exist. This allows to start the server without having to manually create the directory. + - The default database URL is now computed from the configuration directory, instead of being hardcoded to `sqlite://./sqlpage/sqlpage.db`. So when using a custom configuration directory, the default SQLite database will be created inside it. When using the default `./sqlpage` configuration directory, or when using a custom database URL, the default behavior is unchanged. + - New `navbar_title` property in the [shell](https://sql-page.com/documentation.sql?component=shell#component) component to set the title of the top navigation bar. This allows to display a different title in the top menu than the one that appears in the tab of the browser. This can also be set to the empty string to hide the title in the top menu, in case you want to display only a logo for instance. + - Fixed: The `font` property in the [shell](https://sql-page.com/documentation.sql?component=shell#component) component was mistakingly not applied since v0.28.0. It works again. + - Updated SQL parser to [v0.51.0](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0510-2024-09-11). Improved `INTERVAL` parsing. + - **Important note**: this version removes support for the `SET $variable = ...` syntax in SQLite. This worked only with some databases. You should replace all occurrences of this syntax with `SET variable = ...` (without the `$` prefix). + - slightly reduce the margin at the top of pages to make the content appear higher on the screen. + - fix the display of the page title when it is long and the sidebar display is enabled. + - Fix an issue where the color name `blue` could not be used in the chart component. + - **divider component**: Add new properties to the divider component: `link`, `bold`, `italics`, `underline`, `size`. + - ![image](https://github.com/user-attachments/assets/1aced068-7650-42d6-b9bf-2b4631a63c70) + - **form component**: fix slight misalignment and sizing issues of checkboxes and radio buttons. + - ![image](https://github.com/user-attachments/assets/2caf6c28-b1ef-4743-8ffa-351e88c82070) + - **table component**: fixed a bug where markdown contents of table cells would not be rendered as markdown if the column name contained uppercase letters on Postgres. Column name matching is now case-insensitive, so `'title' as markdown` will work the same as `'Title' as markdown`. In postgres, non-double-quoted identifiers are always folded to lowercase. + - **shell component**: fixed a bug where the mobile menu would display even when no menu items were provided. + +## 0.28.0 (2024-08-31) +- Chart component: fix the labels of pie charts displaying too many decimal places. + - ![pie chart](https://github.com/user-attachments/assets/6cc4a522-b9dd-4005-92bc-dc92b16c7293) +- You can now create a `404.sql` file anywhere in your SQLPage project to handle requests to non-existing pages. This allows you to create custom 404 pages, or create [nice URLs](https://sql-page.com/your-first-sql-website/custom_urls.sql) that don't end with `.sql`. + - Create if `/folder/404.sql` exists, then it will be called for all URLs that start with `folder` and do not match an existing file. +- Updated SQL parser to [v0.50.0](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0500-2024-08-15) + - Support postgres String Constants with Unicode Escapes, like `U&'\2713'`. Fixes https://github.com/sqlpage/SQLPage/discussions/511 +- New [big_number](https://sql-page.com/documentation.sql?component=big_number#component) component to display key statistics and indicators in a large, easy-to-read format. Useful for displaying KPIs, metrics, and other important numbers in dashboards and reports. + - ![big_number](https://github.com/user-attachments/assets/9b5bc091-afd1-4872-be55-0b2a47aff15c) +- Fixed small display inconsistencies in the shell component with the new sidebar feature ([#556](https://github.com/sqlpage/SQLPage/issues/556)). +- Cleanly close all open database connections when shutting down sqlpage. Previously, when shutting down SQLPage, database connections that were opened during the session were not explicitly closed. These connections could remain open until the database closes it. Now, SQLPage ensures that all opened database connections are cleanly closed during shutdown. This guarantees that resources are freed immediately, ensuring more reliable operation, particularly in environments with limited database connections. + +## 0.27.0 (2024-08-17) - updated Apex Charts to v3.52.0 - see https://github.com/apexcharts/apexcharts.js/releases - Fixed a bug where in very specific conditions, sqlpage functions could mess up the order of the arguments passed to a sql query. This would happen when a sqlpage function was called with both a column from the database and a sqlpage variable in its arguments, and the query also contained references to other sqlpage variables **after** the sqlpage function call. An example would be `select sqlpage.exec('xxx', some_column = $a) as a, $b as b from t`. A test was added for this case. -- added a new `url_encode` helper for [custom components](https://sql.ophir.dev/custom_components.sql) to encode a string for use in a URL. +- added a new `url_encode` helper for [custom components](https://sql-page.com/custom_components.sql) to encode a string for use in a URL. - fixed a bug where the CSV component would break when the data contained a `#` character. - properly escape fields in the CSV component to avoid generating invalid CSV files. - Nicer inline code style in markdown. - Fixed `width` attribute in the card component not being respected when the specified width was < 6. - Fixed small inaccuracies in decimal numbers leading to unexpectedly long numbers in the output, such as `0.47000000000000003` instead of `0.47`. -- TreeMap charts in the chart component allow you to visualize hierarchical data structures. +- [chart component](https://sql-page.com/documentation.sql?component=chart#component) + - TreeMap charts in the chart component allow you to visualize hierarchical data structures. + - Timeline charts allow you to visualize time intervals. + - Fixed multiple small display issues in the chart component. + - When no series name nor top-level `title` is provided, display the series anyway (with no name) instead of throwing an error in the javascript console. +- Better error handling: Stop processing the SQL file after the first error is encountered. + - The previous behavior was to try paresing a new statement after a syntax error, leading to a cascade of irrelevant error messages after a syntax error. +- Allow giving an id to HTML rows in the table component. This allows making links to specific rows in the table using anchor links. (`my-table.sql#myid`) +- Fixed a bug where long menu items in the shell component's menu would wrap on multiple lines. +- Much better error messages when a call to sqlpage.fetch fails. ## 0.26.0 (2024-08-06) ### Components #### Card -New `width` attribute in the [card](https://sql.ophir.dev/documentation.sql?component=card#component) component to set the width of the card. This finally allows you to create custom layouts, by combining the `embed` and `width` attributes of the card component! This also updates the default layout of the card component: when `columns` is not set, there is now a default of 4 columns instead of 5. +New `width` attribute in the [card](https://sql-page.com/documentation.sql?component=card#component) component to set the width of the card. This finally allows you to create custom layouts, by combining the `embed` and `width` attributes of the card component! This also updates the default layout of the card component: when `columns` is not set, there is now a default of 4 columns instead of 5. ![image](https://github.com/user-attachments/assets/98425bd8-c576-4628-9ae2-db3ba4650019) #### Datagrid -fix [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) color pills display when they contain long text. +fix [datagrid](https://sql-page.com/documentation.sql?component=datagrid#component) color pills display when they contain long text. ![image](https://github.com/user-attachments/assets/3b7dba27-8812-410c-a383-2b62d6a286ac) @@ -45,12 +828,12 @@ Fixed the link to the website title in the shell component. Allow loading javascript ESM modules in the shell component with the new `javascript_module` property. #### html -Added `text` and `post_html` properties to the [html](https://sql.ophir.dev/documentation.sql?component=html#component) component. This allows to include sanitized user-generated content in the middle of custom HTML. +Added `text` and `post_html` properties to the [html](https://sql-page.com/documentation.sql?component=html#component) component. This allows to include sanitized user-generated content in the middle of custom HTML. ```sql -select +select 'html' as component; -select +select 'Username: ' as html, 'username that will be safely escaped: <"& ' as text, '' as post_html; @@ -70,7 +853,7 @@ select - hero component: allow reversing the order of text and images. Allows hero components with the text on the right and the image on the left. - Reduce the max item width in the datagrid component for a better and more compact display on small screens. This makes the datagrid component more mobile-friendly. If you have a datagrid with long text items, this may impact the layout of your page. You can override this behavior by manually changing the `--tblr-datagrid-item-width` CSS variable in your custom CSS. - Apply migrations before initializing the on-database file system. This allows migrations to create files in the database file system. -- Added a [new example](https://github.com/lovasoa/SQLpage/tree/main/examples/CRUD%20-%20Authentication) to the documentation +- Added a [new example](https://github.com/sqlpage/SQLPage/tree/main/examples/CRUD%20-%20Authentication) to the documentation - Bug fix: points with a latitude of 0 are now displayed correctly on the map component. - Bug fix: in sqlite, lower(NULL) now returns NULL instead of an empty string. This is consistent with the standard behavior of lower() in other databases. SQLPage has its own implementation of lower() that supports unicode characters, and our implementation now matches the standard behavior of lower() in mainstream SQLite. - Allow passing data from the database to sqlpage functions. @@ -84,15 +867,15 @@ select - upport UPDATE statements that contain tuple assignments , like `UPDATE table SET (a, b) = (SELECT 1, 2)` - support custom operators in postgres. Usefull when using extensions like PostGIS, PGroonga, pgtrgm, or pg_similarity, which define custom operators like `&&&`, `@>`, `<->`, `~>`, `~>=`, `~<=`, `<@`... - New `html` component to display raw HTML content. This component is meant to be used by advanced users who want to display HTML content that cannot be expressed with the other components. Make sure you understand the security implications before using this component, as using untrusted HTML content can expose your users to [cross-site scripting (XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. -- New parameter in the [`run_sql`](https://sql.ophir.dev/functions.sql?function=run_sql#function) function to pass variables to the included SQL file, instead of using the global variables. Together with the new ability to pass data from the database to SQLPage functions, this allows you to create more modular and reusable SQL files. For instance, the following is finally possible: +- New parameter in the [`run_sql`](https://sql-page.com/functions.sql?function=run_sql#function) function to pass variables to the included SQL file, instead of using the global variables. Together with the new ability to pass data from the database to SQLPage functions, this allows you to create more modular and reusable SQL files. For instance, the following is finally possible: ```sql select 'dynamic' as component, sqlpage.run_sql('display_product.sql', json_object('product_id', product_id)) as properties from products; ``` - New icons (see [tabler icons 3.10](https://tabler.io/changelog)) - Updated apexcharts.js to [v3.50.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v3.50.0) - Improve truncation of long page titles - - ![screenshot long title](https://github.com/lovasoa/SQLpage/assets/552629/9859023e-c706-47b3-aa9e-1c613046fdfa) -- new function: [`sqlpage.link`](https://sql.ophir.dev/functions.sql?function=link#function) to easily create links with parameters between pages. For instance, you can now use + - ![screenshot long title](https://github.com/sqlpage/SQLPage/assets/552629/9859023e-c706-47b3-aa9e-1c613046fdfa) +- new function: [`sqlpage.link`](https://sql-page.com/functions.sql?function=link#function) to easily create links with parameters between pages. For instance, you can now use ```sql select 'list' as component; select @@ -108,28 +891,28 @@ select ## 0.24.0 (2024-06-23) - in the form component, searchable `select` fields now support more than 50 options. They used to display only the first 50 options. - - ![screenshot](https://github.com/lovasoa/SQLpage/assets/552629/40571d08-d058-45a8-83ef-91fa134f7ce2) + - ![screenshot](https://github.com/sqlpage/SQLPage/assets/552629/40571d08-d058-45a8-83ef-91fa134f7ce2) - map component - automatically center the map on the contents when no top-level latitude and longitude properties are provided even when the map contains geojson data. - allow using `FALSE as tile_source` to completely remove the base map. This makes the map component useful to display even non-geographical geometric data. - Fix a bug that occured when no `database_url` was provided in the configuration file. SQLPage would generate an incorrect default SQLite database URL. -- Add a new `background_color` attribute to the [card](https://sql.ophir.dev/documentation.sql?component=card#component) component to set the background color of the card. - - ![cards with color backgrounds](https://github.com/lovasoa/SQLpage/assets/552629/d925d77c-e1f6-490f-8fb4-cdcc4418233f) -- new handlebars helper for [custom components](https://sql.ophir.dev/custom_components.sql): `{{app_config 'property'}}` to access the configuration object from the handlebars template. +- Add a new `background_color` attribute to the [card](https://sql-page.com/documentation.sql?component=card#component) component to set the background color of the card. + - ![cards with color backgrounds](https://github.com/sqlpage/SQLPage/assets/552629/d925d77c-e1f6-490f-8fb4-cdcc4418233f) +- new handlebars helper for [custom components](https://sql-page.com/custom_components.sql): `{{app_config 'property'}}` to access the configuration object from the handlebars template. - Prevent form validation and give a helpful error message when an user tries to submit a form with a file upload field that is above the maximum file size. - - ![file upload too large](https://github.com/lovasoa/SQLpage/assets/552629/1c684d33-49bd-4e49-9ee0-ed3f0d454ced) -- Fix a bug in [`sqlpage.read_file_as_data_url`](https://sql.ophir.dev/functions.sql?function=read_file_as_data_url#function) where it would truncate the mime subtype of the file. This would cause the browser to refuse to display SVG files, for instance. + - ![file upload too large](https://github.com/sqlpage/SQLPage/assets/552629/1c684d33-49bd-4e49-9ee0-ed3f0d454ced) +- Fix a bug in [`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file_as_data_url#function) where it would truncate the mime subtype of the file. This would cause the browser to refuse to display SVG files, for instance. - Avoid vertical scrolling caused by the footer even when the page content is short. -- Add a new `compact` attribute to the [list](https://sql.ophir.dev/documentation.sql?component=list#component), allowing to display more items in a list without taking up too much space. Great for displaying long lists of items. - - ![compact list screenshot](https://github.com/lovasoa/SQLpage/assets/552629/41302807-c6e4-40a0-9486-bfd0ceae1537) -- Add property `narrow` to the [button](https://sql.ophir.dev/documentation.sql?component=button#component) component to make the button narrower. Ideal for buttons with icons. - - ![icon buttons](https://github.com/lovasoa/SQLpage/assets/552629/7fcc049e-6012-40c1-a8ee-714ce70a8763) +- Add a new `compact` attribute to the [list](https://sql-page.com/documentation.sql?component=list#component), allowing to display more items in a list without taking up too much space. Great for displaying long lists of items. + - ![compact list screenshot](https://github.com/sqlpage/SQLPage/assets/552629/41302807-c6e4-40a0-9486-bfd0ceae1537) +- Add property `narrow` to the [button](https://sql-page.com/documentation.sql?component=button#component) component to make the button narrower. Ideal for buttons with icons. + - ![icon buttons](https://github.com/sqlpage/SQLPage/assets/552629/7fcc049e-6012-40c1-a8ee-714ce70a8763) - new `tooltip` property in the datagrid component. - - ![datagrid tooltip](https://github.com/lovasoa/SQLpage/assets/552629/81b94d92-1bca-4ffe-9056-c30d6845dcc6) + - ![datagrid tooltip](https://github.com/sqlpage/SQLPage/assets/552629/81b94d92-1bca-4ffe-9056-c30d6845dcc6) - datagrids are now slightly more compact, with less padding and less space taken by each item. -- fix a bug in the [card](https://sql.ophir.dev/documentation.sql?component=card#component) component where the icon would sometimes overflow the card's text content. -- new `image` property in the [button](https://sql.ophir.dev/documentation.sql?component=button#component) component to display a small image inside a button. - - ![image button](https://github.com/lovasoa/SQLpage/assets/552629/cdfa0709-1b00-4779-92cb-dc6f3e78c1a8) +- fix a bug in the [card](https://sql-page.com/documentation.sql?component=card#component) component where the icon would sometimes overflow the card's text content. +- new `image` property in the [button](https://sql-page.com/documentation.sql?component=button#component) component to display a small image inside a button. + - ![image button](https://github.com/sqlpage/SQLPage/assets/552629/cdfa0709-1b00-4779-92cb-dc6f3e78c1a8) - In the `shell` component - allow easily creating complex menus even in SQLite: ```sql @@ -139,11 +922,11 @@ select ```sql select 'shell' as component, 'My Website' as title, CASE WHEN $role = 'admin' THEN 'Admin' END as menu_item; ``` - - Add the ability to use local Woff2 fonts in the [shell](https://sql.ophir.dev/documentation.sql?component=shell#component) component. This is useful to use custom fonts in your website, without depending on google fonts (and disclosing your users' IP addresses to google). + - Add the ability to use local Woff2 fonts in the [shell](https://sql-page.com/documentation.sql?component=shell#component) component. This is useful to use custom fonts in your website, without depending on google fonts (and disclosing your users' IP addresses to google). - Add a `fixed_top_menu` attribute to make the top menu sticky. This is useful to keep the menu visible even when the user scrolls down the page. - - ![a fixed top menu](https://github.com/lovasoa/SQLpage/assets/552629/65fe3a41-faee-45e6-9dfc-d81eca043f45) + - ![a fixed top menu](https://github.com/sqlpage/SQLPage/assets/552629/65fe3a41-faee-45e6-9dfc-d81eca043f45) - Add a `wrap` attribute to the `list` component to wrap items on multiple lines when they are too long. -- New `max_pending_rows` [configuration option](https://sql.ophir.dev/configuration.md) to limit the number of messages that can be sent to the client before they are read. Usefule when sending large amounts of data to slow clients. +- New `max_pending_rows` [configuration option](https://sql-page.com/configuration.md) to limit the number of messages that can be sent to the client before they are read. Usefule when sending large amounts of data to slow clients. - New `compress_responses` configuration option. Compression is still on by default, but can now be disabled to allow starting sending the page sooner. It's sometimes better to start displaying the shell immediateley and render components as soon as they are ready, even if that means transmitting more data over the wire. - Update sqlite to v3.46: https://www.sqlite.org/releaselog/3_46_0.html - major upgrades to PRAGMA optimize, making it smarter and more efficient on large databases @@ -152,31 +935,31 @@ select - new [`json_pretty()`](https://www.sqlite.org/json1.html) function - Faster initial page load. SQLPage used to wait for the first component to be rendered before sending the shell to the client. We now send the shell immediately, and the first component as soon as it is ready. This can make the initial page load faster, especially when the first component requires a long computation on the database side. - Include a default favicon when none is specified in the shell component. This fixes the `Unable to read file "favicon.ico"` error message that would appear in the logs by default. - - ![favicon](https://github.com/lovasoa/SQLpage/assets/552629/cf48e271-2fe4-42da-b825-893cff3f95fb) + - ![favicon](https://github.com/sqlpage/SQLPage/assets/552629/cf48e271-2fe4-42da-b825-893cff3f95fb) ## 0.23.0 (2024-06-09) -- fix a bug in the [csv](https://sql.ophir.dev/documentation.sql?component=csv#component) component. The `separator` parameter now works as expected. This facilitates creating excel-compatible CSVs in european countries where excel expects the separator to be `;` instead of `,`. +- fix a bug in the [csv](https://sql-page.com/documentation.sql?component=csv#component) component. The `separator` parameter now works as expected. This facilitates creating excel-compatible CSVs in european countries where excel expects the separator to be `;` instead of `,`. - new `tooltip` property in the button component. - New `search_value` property in the shell component. - Fixed a display issue in the hero component when the button text is long and the viewport is narrow. - reuse the existing opened database connection for the current query in `sqlpage.run_sql` instead of opening a new one. This makes it possible to create a temporary table in a file, and reuse it in an included script, create a SQL transaction that spans over multiple run_sql calls, and should generally make run_sql more performant. - Fixed a bug in the cookie component where removing a cookie from a subdirectory would not work. - [Updated SQL parser](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0470-2024-06-01). Fixes support for `AT TIME ZONE` in postgres. Fixes `GROUP_CONCAT()` in MySQL. -- Add a new warning message in the logs when trying to use `SET $x = ` when there is already a form field named `x`. +- Add a new warning message in the logs when trying to use `set x = ` when there is already a form field named `x`. - **Empty Uploaded files**: when a form contains an optional file upload field, and the user does not upload a file, the field used to still be accessible to SQLPage file-related functions such as `sqlpage.uploaded_file_path` and `sqlpage.uploaded_file_mime_type`. This is now fixed, and these functions will return `NULL` when the user does not upload a file. `sqlpage.persist_uploaded_file` will not create an empty file in the target directory when the user does not upload a file, instead it will do nothing and return `NULL`. -- In the [map](https://sql.ophir.dev/documentation.sql?component=map#component) component, when top-level latitude and longitude properties are omitted, the map will now center on its markers. This makes it easier to create zoomed maps with a single marker. -- In the [button](https://sql.ophir.dev/documentation.sql?component=button#component) component, add a `download` property to make the button download a file when clicked, a `target` property to open the link in a new tab, and a `rel` property to prevent search engines from following the link. -- New `timeout` option in the [sqlpage.fetch](https://sql.ophir.dev/functions.sql?function=fetch#function) function to set a timeout for the request. This is useful when working with slow or unreliable APIs, large payloads, or when you want to avoid waiting too long for a response. -- In the [hero](https://sql.ophir.dev/documentation.sql?component=hero#component) component, add a `poster` property to display a video poster image, a `loop` property to loop the video (useful for short animations), a `muted` property to mute the video, and a `nocontrols` property to hide video controls. +- In the [map](https://sql-page.com/documentation.sql?component=map#component) component, when top-level latitude and longitude properties are omitted, the map will now center on its markers. This makes it easier to create zoomed maps with a single marker. +- In the [button](https://sql-page.com/documentation.sql?component=button#component) component, add a `download` property to make the button download a file when clicked, a `target` property to open the link in a new tab, and a `rel` property to prevent search engines from following the link. +- New `timeout` option in the [sqlpage.fetch](https://sql-page.com/functions.sql?function=fetch#function) function to set a timeout for the request. This is useful when working with slow or unreliable APIs, large payloads, or when you want to avoid waiting too long for a response. +- In the [hero](https://sql-page.com/documentation.sql?component=hero#component) component, add a `poster` property to display a video poster image, a `loop` property to loop the video (useful for short animations), a `muted` property to mute the video, and a `nocontrols` property to hide video controls. - Fix a bug where icons would disappear when serving a SQLPage website from a subdirectory and not the root of the (sub)domain using the `site_prefix` configuration option. ## 0.22.0 (2024-05-29) -- **Important Security Fix:** The behavior of `SET $x` has been modified to match `SELECT $x`. - - **Security Risk:** Previously, `SET $x` could be overwritten by a POST parameter named `x`. - - **Solution:** Upgrade to SQLPage v0.22. If not possible, then update your application to use `SET :x` instead of `SET $x`. - - For more information, see [GitHub Issue #342](https://github.com/lovasoa/SQLpage/issues/342). +- **Important Security Fix:** The behavior of `set x` has been modified to match `SELECT $x`. + - **Security Risk:** Previously, `set x` could be overwritten by a POST parameter named `x`. + - **Solution:** Upgrade to SQLPage v0.22. If not possible, then update your application to use `SET :x` instead of `set x`. + - For more information, see [GitHub Issue #342](https://github.com/sqlpage/SQLPage/issues/342). - **Deprecation Notice:** Reading POST variables using `$x`. - **New Standard:** Use `:x` for POST variables and `$x` for GET variables. - **Current Release Warning:** Using `$x` for POST variables will display a console warning: @@ -188,23 +971,23 @@ select - **Reminder about GET and POST Variables:** - **GET Variables:** Parameters included in the URL of an HTTP GET request, used to retrieve data. Example: `https://example.com/page?x=value`, where `x` is a GET variable. - **POST Variables:** Parameters included in the body of an HTTP POST request, used for form submissions. Example: the value entered by the user in a form field named `x`. -- Two **backward-incompatible changes** in the [chart](https://sql.ophir.dev/documentation.sql?component=chart#component) component's timeseries plotting feature (actioned with `TRUE as time`): +- Two **backward-incompatible changes** in the [chart](https://sql-page.com/documentation.sql?component=chart#component) component's timeseries plotting feature (actioned with `TRUE as time`): - when providing a number for the x value (time), it is now interpreted as a unix timestamp, in seconds (number of seconds since 1970-01-01 00:00:00 UTC). It used to be interpreted as milliseconds. If you were using the `TRUE as time` syntax with integer values, you will need to divide your time values by 1000 to get the same result as before. - This change makes it easier to work with time series plots, as most databases return timestamps in seconds. For instance, in SQLite, you can store timestamps as integers with the [`unixepoch()`](https://www.sqlite.org/lang_datefunc.html) function, and plot them directly in SQLPage. - - when providing an ISO datetime string for the x value (time), without an explicit timezone, it is now interpreted and displayed in the local timezone of the user. It used to be interpreted as a local time, but displayed in UTC, which [was confusing](https://github.com/lovasoa/SQLpage/issues/324). If you were using the `TRUE as time` syntax with naive datetime strings (without timezone information), you will need to convert your datetime strings to UTC on the database side if you want to keep the same behavior as before. As a side note, it is always recommended to store and query datetime strings with timezone information in the database, to avoid ambiguity. + - when providing an ISO datetime string for the x value (time), without an explicit timezone, it is now interpreted and displayed in the local timezone of the user. It used to be interpreted as a local time, but displayed in UTC, which [was confusing](https://github.com/sqlpage/SQLPage/issues/324). If you were using the `TRUE as time` syntax with naive datetime strings (without timezone information), you will need to convert your datetime strings to UTC on the database side if you want to keep the same behavior as before. As a side note, it is always recommended to store and query datetime strings with timezone information in the database, to avoid ambiguity. - This change is particularly useful in SQLite, which generates naive datetime strings by default. You should still store and query datetimes as unix timestamps when possible, to avoid ambiguity and reduce storage size. -- When calling a file with [`sqlpage.run_sql`](https://sql.ophir.dev/functions.sql?function=run_sql#function), the target file now has access to uploaded files. -- New article by [Matthew Larkin](https://github.com/matthewlarkin) about [migrations](https://sql.ophir.dev/your-first-sql-website/migrations.sql). +- When calling a file with [`sqlpage.run_sql`](https://sql-page.com/functions.sql?function=run_sql#function), the target file now has access to uploaded files. +- New article by [Matthew Larkin](https://github.com/matthewlarkin) about [migrations](https://sql-page.com/your-first-sql-website/migrations.sql). - Add a row-level `id` attribute to the button component. - Static assets (js, css, svg) needed to build SQLPage are now cached individually, and can be downloaded separately from the build process. This makes it easier to build SQLPage without internet access. If you use pre-built SQLPage binaries, this change does not affect you. - New `icon_after` row-level property in the button component to display an icon on the right of a button (after the text). Contributed by @amrutadotorg. - New demo example: [dark theme](./examples/light-dark-toggle/). Contributed by @lyderic. -- Add the ability to [bind to a unix socket instead of a TCP port](https://sql.ophir.dev/your-first-sql-website/nginx.sql) for better performance on linux. Contributed by @vlasky. +- Add the ability to [bind to a unix socket instead of a TCP port](https://sql-page.com/your-first-sql-website/nginx.sql) for better performance on linux. Contributed by @vlasky. ## 0.21.0 (2024-05-19) - `sqlpage.hash_password(NULL)` now returns `NULL` instead of throwing an error. This behavior was changed unintentionally in 0.20.5 and could have broken existing SQLPage websites. -- The [dynamic](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component now supports multiple `properties` attributes. The following is now possible: +- The [dynamic](https://sql-page.com/documentation.sql?component=dynamic#component) component now supports multiple `properties` attributes. The following is now possible: ```sql select 'dynamic' as component, '{ "component": "card", "title": "Hello" }' as properties, @@ -213,9 +996,9 @@ select - Casting values from one type to another using the `::` operator is only supported by PostgreSQL. SQLPage versions before 0.20.5 would silently convert all casts to the `CAST(... AS ...)` syntax, which is supported by all databases. Since 0.20.5, SQLPage started to respect the original `::` syntax, and pass it as-is to the database. This broke existing SQLPage websites that used the `::` syntax with databases other than PostgreSQL. For backward compatibility, this version of SQLPage re-establishes the previous behavior, converts `::` casts on non-PostgreSQL databases to the `CAST(... AS ...)` syntax, but will display a warning in the logs. - In short, if you saw an error like `Error: unrecognized token ":"` after upgrading to 0.20.5, this version should fix it. - The `dynamic` component now properly displays error messages when its properties are invalid. There used to be a bug where errors would be silently ignored, making it hard to debug invalid dynamic components. -- New [`sqlpage.request_method`](https://sql.ophir.dev/functions.sql?function=request_method#function) function to get the HTTP method used to access the current page. This is useful to create pages that behave differently depending on whether they are accessed with a GET request (to display a form, for instance) or a POST request (to process the form). -- include the trailing semicolon as a part of the SQL statement sent to the database. This doesn't change anything in most databases, but Microsoft SQL Server requires a trailing semicolon after certain statements, such as `MERGE`. Fixes [issue #318](https://github.com/lovasoa/SQLpage/issues/318) -- New `readonly` and `disabled` attributes in the [form](https://sql.ophir.dev/documentation.sql?component=form#component) component to make form fields read-only or disabled. This is useful to prevent the user from changing some fields. +- New [`sqlpage.request_method`](https://sql-page.com/functions.sql?function=request_method#function) function to get the HTTP method used to access the current page. This is useful to create pages that behave differently depending on whether they are accessed with a GET request (to display a form, for instance) or a POST request (to process the form). +- include the trailing semicolon as a part of the SQL statement sent to the database. This doesn't change anything in most databases, but Microsoft SQL Server requires a trailing semicolon after certain statements, such as `MERGE`. Fixes [issue #318](https://github.com/sqlpage/SQLPage/issues/318) +- New `readonly` and `disabled` attributes in the [form](https://sql-page.com/documentation.sql?component=form#component) component to make form fields read-only or disabled. This is useful to prevent the user from changing some fields. - 36 new icons [(tabler icons 3.4)](https://tabler.io/icons/changelog) - Bug fixes in charts [(apexcharts.js v3.49.1)](https://github.com/apexcharts/apexcharts.js/releases) @@ -231,7 +1014,7 @@ select - The SQLPage function system was greatly improved - All the functions can now be freely combined and nested, without any limitation. No more `Expected a literal single quoted string.` errors when trying to nest functions. - The error messages when a function call is invalid were rewritten, to include more context, and provide suggestions on how to fix the error. This should make it easier get started with SQLPage functions. - Error messages should always be clear and actionnable. If you encounter an error message you don't understand, please [open an issue](https://github.com/lovasoa/SQLpage/issues) on the SQLPage repository. + Error messages should always be clear and actionnable. If you encounter an error message you don't understand, please [open an issue](https://github.com/sqlpage/SQLPage/issues) on the SQLPage repository. - Adding new functions is now easier, and the code is more maintainable. This should make it easier to contribute new functions to SQLPage. If you have an idea for a new function, feel free to open an issue or a pull request on the SQLPage repository. All sqlpage functions are defined in [`functions.rs`](./src/webserver/database/sqlpage_functions/functions.rs). - The `shell-empty` component (used to create pages without a shell) now supports the `html` attribute, to directly set the raw contents of the page. This is useful to advanced users who want to generate the page content directly in SQL, without using the SQLPage components. - Updated sqlparser to [v0.46](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0460-2024-05-03) @@ -246,10 +1029,10 @@ select ## 0.20.3 (2024-04-22) -- New `dropdown` row-level property in the [`form` component](https://sql.ophir.dev/documentation.sql?component=form#component) - - ![select dropdown in form](https://github.com/lovasoa/SQLpage/assets/552629/5a2268d3-4996-49c9-9fb5-d310e753f844) - - ![multiselect input](https://github.com/lovasoa/SQLpage/assets/552629/e8d62d1a-c851-4fef-8c5c-a22991ffadcf) -- Adds a new [`sqlpage.fetch`](https://sql.ophir.dev/functions.sql?function=fetch#function) function that allows sending http requests from SQLPage. This is useful to query external APIs. This avoids having to resort to `sqlpage.exec`. +- New `dropdown` row-level property in the [`form` component](https://sql-page.com/documentation.sql?component=form#component) + - ![select dropdown in form](https://github.com/sqlpage/SQLPage/assets/552629/5a2268d3-4996-49c9-9fb5-d310e753f844) + - ![multiselect input](https://github.com/sqlpage/SQLPage/assets/552629/e8d62d1a-c851-4fef-8c5c-a22991ffadcf) +- Adds a new [`sqlpage.fetch`](https://sql-page.com/functions.sql?function=fetch#function) function that allows sending http requests from SQLPage. This is useful to query external APIs. This avoids having to resort to `sqlpage.exec`. - Fixed a bug that occured when using both HTTP and HTTPS in the same SQLPage instance. SQLPage tried to bind to the same (HTTP) port twice instead of binding to the HTTPS port. This is now fixed, and SQLPage can now be used with both a non-443 `port` and an `https_domain` set in the configuration file. @@ -262,8 +1045,8 @@ select ## 0.20.2 (2024-04-01) -- the **default component**, used when no `select '...' as component` is present, is now [table](https://sql.ophir.dev/documentation.sql?component=table#component). It used to be the `debug` component instead. `table` makes it extremely easy to display the results of any SQL query in a readable manner. Just write any query in a `.sql` file open it in your browser, and you will see the results displayed in a table, without having to use any SQLPage-specific column names or attributes. -- Better error messages when a [custom component](https://sql.ophir.dev/custom_components.sql) contains a syntax error. [Fix contributed upstream](https://github.com/sunng87/handlebars-rust/pull/638) +- the **default component**, used when no `select '...' as component` is present, is now [table](https://sql-page.com/documentation.sql?component=table#component). It used to be the `debug` component instead. `table` makes it extremely easy to display the results of any SQL query in a readable manner. Just write any query in a `.sql` file open it in your browser, and you will see the results displayed in a table, without having to use any SQLPage-specific column names or attributes. +- Better error messages when a [custom component](https://sql-page.com/custom_components.sql) contains a syntax error. [Fix contributed upstream](https://github.com/sunng87/handlebars-rust/pull/638) - Lift a limitation on **sqlpage function nesting**. In previous versions, some sqlpage functions could not be used inside other sqlpage functions. For instance, `sqlpage.url_encode(sqlpage.exec('my_program'))` used to throw an error saying `Nested exec() function not allowed`. This limitation is now lifted, and you can nest any sqlpage function inside any other sqlpage function. - Allow **string concatenation in inside sqlpage function parameters**. For instance, `sqlpage.exec('echo', 'Hello ' || 'world')` is now supported, whereas it used to throw an error saying `exec('echo', 'Hello ' || 'world') is not a valid call. Only variables (such as $my_variable) and sqlpage function calls (such as sqlpage.header('my_header')) are supported as arguments to sqlpage functions.`. - Bump the minimal supported rust version to 1.77 (this is what allows us to easily handle nested sqlpage functions) @@ -271,21 +1054,21 @@ select ## 0.20.1 (2024-03-23) - More than 200 new icons, with [tabler icons v3](https://tabler.io/icons/changelog#3.0) -- New [`sqlpage.persist_uploaded_file`](https://sql.ophir.dev/functions.sql?function=persist_uploaded_file#function) function to save uploaded files to a permanent location on the local filesystem (where SQLPage is running). This is useful to store files uploaded by users in a safe location, and to serve them back to users later. +- New [`sqlpage.persist_uploaded_file`](https://sql-page.com/functions.sql?function=persist_uploaded_file#function) function to save uploaded files to a permanent location on the local filesystem (where SQLPage is running). This is useful to store files uploaded by users in a safe location, and to serve them back to users later. - Correct error handling for file uploads. SQLPage used to silently ignore file uploads that failed (because they exceeded [max_uploaded_file_size](./configuration.md), for instance), but now it displays a clear error message to the user. ## 0.20.0 (2024-03-12) -- **file inclusion**. This is a long awaited feature that allows you to include the contents of one file in another. This is useful to factorize common parts of your website, such as the header, or the authentication logic. There is a new [`sqlpage.run_sql`](https://sql.ophir.dev/functions.sql?function=run_sql#function) function that runs a given SQL file and returns its result as a JSON array. Combined with the existing [`dynamic`](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component, this allows you to include the content of a file in another, like this: +- **file inclusion**. This is a long awaited feature that allows you to include the contents of one file in another. This is useful to factorize common parts of your website, such as the header, or the authentication logic. There is a new [`sqlpage.run_sql`](https://sql-page.com/functions.sql?function=run_sql#function) function that runs a given SQL file and returns its result as a JSON array. Combined with the existing [`dynamic`](https://sql-page.com/documentation.sql?component=dynamic#component) component, this allows you to include the content of a file in another, like this: ```sql select 'dynamic' as component, sqlpage.run_sql('header.sql') as properties; ``` -- **more powerful _dynamic_ component**: the [`dynamic`](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component can now be used to generate the special _header_ components too, such as the `redirect`, `cookie`, `authentication`, `http_header` and `json` components. The _shell_ component used to be allowed in dynamic components, but only if they were not nested (a dynamic component inside another one). This limitation is now lifted. This is particularly useful in combination with the new file inclusion feature, to factorize common parts of your website. There used to be a limited to how deeply nested dynamic components could be, but this limitation is now lifted too. -- Add an `id` attribute to form fields in the [form](https://sql.ophir.dev/documentation.sql?component=form#component) component. This allows you to easily reference form fields in custom javascript code. -- New [`rss`](https://sql.ophir.dev/documentation.sql?component=rss#component) component to create RSS feeds, including **podcast feeds**. You can now create and manage your podcast feed entirely in SQL, and distribute it to all podcast directories such as Apple Podcasts, Spotify, and Google Podcasts. -- Better error handling in template rendering. Many template helpers now display a more precise error message when they fail to execute. This makes it easier to debug errors when you [develop your own custom components](https://sql.ophir.dev/custom_components.sql). +- **more powerful _dynamic_ component**: the [`dynamic`](https://sql-page.com/documentation.sql?component=dynamic#component) component can now be used to generate the special _header_ components too, such as the `redirect`, `cookie`, `authentication`, `http_header` and `json` components. The _shell_ component used to be allowed in dynamic components, but only if they were not nested (a dynamic component inside another one). This limitation is now lifted. This is particularly useful in combination with the new file inclusion feature, to factorize common parts of your website. There used to be a limited to how deeply nested dynamic components could be, but this limitation is now lifted too. +- Add an `id` attribute to form fields in the [form](https://sql-page.com/documentation.sql?component=form#component) component. This allows you to easily reference form fields in custom javascript code. +- New [`rss`](https://sql-page.com/documentation.sql?component=rss#component) component to create RSS feeds, including **podcast feeds**. You can now create and manage your podcast feed entirely in SQL, and distribute it to all podcast directories such as Apple Podcasts, Spotify, and Google Podcasts. +- Better error handling in template rendering. Many template helpers now display a more precise error message when they fail to execute. This makes it easier to debug errors when you [develop your own custom components](https://sql-page.com/custom_components.sql). - better error messages when an error occurs when defining a variable with `SET`. SQLPage now displays the query that caused the error, and the name of the variable that was being defined. - Updated SQL parser to [v0.44](https://github.com/sqlparser-rs/sqlparser-rs/blob/main/CHANGELOG.md#0440-2024-03-02) - support [EXECUTE ... USING](https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN) in PostgreSQL @@ -304,24 +1087,24 @@ select 'dynamic' as component, sqlpage.run_sql('header.sql') as properties; The configuration directory is where SQLPage looks for the `sqlpage.json` configuration file, for the `migrations` and `templates` directories, and the `on_connect.sql` file. It used to be hardcoded to `./sqlpage/`, which made each SQLPage invokation dependent on the [current working directory](https://en.wikipedia.org/wiki/Working_directory). Now you can, for instance, set `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` in your environment, and SQLPage will look for its configuration files in `/etc/sqlpage`, which is a more standard location for configuration files in a Unix environment. - The official docker image now sets `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` by default, and changes the working directory to `/var/www/` by default. - - **⚠️ WARNING**: This change can break your docker image if you relied on setting the working directory to `/var/www` and putting the configuration in `/var/www/sqlpage`. In this case, the recommended setup is to store your sqlpage configuration directory and sql files in different directory. For more information see [this issue](https://github.com/lovasoa/SQLpage/issues/246). + - **⚠️ WARNING**: This change can break your docker image if you relied on setting the working directory to `/var/www` and putting the configuration in `/var/www/sqlpage`. In this case, the recommended setup is to store your sqlpage configuration directory and sql files in different directory. For more information see [this issue](https://github.com/sqlpage/SQLPage/issues/246). - Updated the chart component to use the latest version of the charting library - https://github.com/apexcharts/apexcharts.js/releases/tag/v3.45.2 - https://github.com/apexcharts/apexcharts.js/releases/tag/v3.46.0 - Updated Tabler Icon library to v2.47 with new icons - see: https://tabler.io/icons/changelog ![](https://pbs.twimg.com/media/GFUiJa_WsAAd0Td?format=jpg&name=medium) - Added `prefix`, `prefix_icon` and `suffix` attributes to the `form` component to create input groups. Useful to add a currency symbol or a unit to a form input, or to visually illustrate the type of input expected. -- Added `striped_rows`, `striped_columns`, `hover`,`border`, and `small` attributes to the [table component](https://sql.ophir.dev/documentation.sql?component=table#component). +- Added `striped_rows`, `striped_columns`, `hover`,`border`, and `small` attributes to the [table component](https://sql-page.com/documentation.sql?component=table#component). - In the cookie component, set cookies for the entire website by default. The old behavior was to set the cookie only for files inside the current folder by default, which did not match the documentation, that says "If not specified, the cookie will be sent for all paths". - Dynamic components at the top of sql files. - If you have seen _Dynamic components at the top level are not supported, except for setting the shell component properties_ in the past, you can now forget about it. You can now use dynamic components at the top level of your sql files, and they will be interpreted as expected. -- [Custom shells](https://sql.ophir.dev/custom_components.sql): +- [Custom shells](https://sql-page.com/custom_components.sql): - It has always been possible to change the default shell of a SQLPage website by writing a `sqlpage/shell.handlebars` file. But that forced you to have a single shell for the whole website. It is now possible to have multiple shells, just by creating multiple `shell-*.handlebars` files in the `sqlpage` directory. A `shell-empty` file is also provided by default, to create pages without a shell (useful for returning non-html content, such as an RSS feed). - New `edit_link`, `delete_link`, and `view_link` row-level attributes in the list component to add icons and links to each row. - - ![screenshot](https://github.com/lovasoa/SQLpage/assets/552629/df085592-8359-4fed-9aeb-27a2416ab6b8) -- **Multiple page layouts** : The page layout is now configurable from the [shell component](https://sql.ophir.dev/documentation.sql?component=shell#component). 3 layouts are available: `boxed` (the default), `fluid` (full width), and `horizontal` (with boxed contents but a full-width header). - - ![horizontal layout screenshot](https://github.com/lovasoa/SQLpage/assets/552629/3c0fde36-7bf6-414e-b96f-c8880a2fc786) + - ![screenshot](https://github.com/sqlpage/SQLPage/assets/552629/df085592-8359-4fed-9aeb-27a2416ab6b8) +- **Multiple page layouts** : The page layout is now configurable from the [shell component](https://sql-page.com/documentation.sql?component=shell#component). 3 layouts are available: `boxed` (the default), `fluid` (full width), and `horizontal` (with boxed contents but a full-width header). + - ![horizontal layout screenshot](https://github.com/sqlpage/SQLPage/assets/552629/3c0fde36-7bf6-414e-b96f-c8880a2fc786) ## 0.18.3 (2024-02-03) @@ -330,8 +1113,8 @@ select 'dynamic' as component, sqlpage.run_sql('header.sql') as properties; - MySQL's [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html) table-valued function, that allows easily iterating over json structures - MySQL's [`CALL`](https://dev.mysql.com/doc/refman/8.0/en/call.html) statements, to call stored procedures. - PostgreSQL `^@` starts-with operator -- New [carousel](https://sql.ophir.dev/documentation.sql?component=carousel#component) component to display a carousel of images. -- For those who write [custom components](https://sql.ophir.dev/custom_components.sql), a new `@component_index` variable is available in templates to get the index of the current component in the page. This makes it easy to generate unique ids for components. +- New [carousel](https://sql-page.com/documentation.sql?component=carousel#component) component to display a carousel of images. +- For those who write [custom components](https://sql-page.com/custom_components.sql), a new `@component_index` variable is available in templates to get the index of the current component in the page. This makes it easy to generate unique ids for components. ## 0.18.2 (2024-01-29) @@ -344,16 +1127,16 @@ select 'dynamic' as component, sqlpage.run_sql('header.sql') as properties; ## 0.18.0 (2024-01-28) - Fix small display issue on cards without a title. -- New component: [`tracking`](https://sql.ophir.dev/documentation.sql?component=tracking#component) for beautiful and compact status reports. -- New component: [`divider`](https://sql.ophir.dev/documentation.sql?component=divider#component) to add a horizontal line between other components. -- New component: [`breadcrumb`](https://sql.ophir.dev/documentation.sql?component=breadcrumb#component) to display a breadcrumb navigation bar. +- New component: [`tracking`](https://sql-page.com/documentation.sql?component=tracking#component) for beautiful and compact status reports. +- New component: [`divider`](https://sql-page.com/documentation.sql?component=divider#component) to add a horizontal line between other components. +- New component: [`breadcrumb`](https://sql-page.com/documentation.sql?component=breadcrumb#component) to display a breadcrumb navigation bar. - fixed a small visual bug in the `card` component, where the margin below footer text was too large. - new `ystep` top-level attribute in the `chart` component to customize the y-axis step size. - Updated default graph colors so that all series are easily distinguishable even when a large number of series are displayed. - New `embed` attribute in the `card` component that lets you build multi-column layouts of various components with cards. - ![](./examples/cards-with-remote-content/screenshot.png) - Added `id` and `class` attributes to all components, to make it easier to style them with custom CSS and to reference them in intra-page links and custom javascript code. -- Implemented [uploaded_file_mime_type](https://sql.ophir.dev/functions.sql?function=uploaded_file_mime_type#function) +- Implemented [uploaded_file_mime_type](https://sql-page.com/functions.sql?function=uploaded_file_mime_type#function) - Update the built-in SQLite database to version 3.45.0: https://www.sqlite.org/releaselog/3_45_0.html - Add support for unicode in the built-in SQLite database. This includes the `lower` and `upper` functions, and the `NOCASE` collation. @@ -363,18 +1146,18 @@ select 'dynamic' as component, sqlpage.run_sql('header.sql') as properties; This is now fixed, and you can see the HTTP requests again. Logging is still less verbose than before, but you can enable debug logs by setting the `RUST_LOG` environment variable to `debug`, or to `sqlpage=debug` to only see SQLPage debug logs. - Better error message when failing to bind to a low port (<1024) on Linux. SQLPage now displays a message explaining how to allow SQLPage to bind to a low port. - When https_domain is set, but a port number different from 443 is set, SQLPage now starts both an HTTP and an HTTPS server. -- Better error message when component order is invalid. SQLPage has "header" components, such as [redirect](https://sql.ophir.dev/documentation.sql?component=redirect#component) and [cookie](https://sql.ophir.dev/documentation.sql?component=cookie#component), that must be executed before the rest of the page. SQLPage now displays a clear error message when you try to use them after other components. +- Better error message when component order is invalid. SQLPage has "header" components, such as [redirect](https://sql-page.com/documentation.sql?component=redirect#component) and [cookie](https://sql-page.com/documentation.sql?component=cookie#component), that must be executed before the rest of the page. SQLPage now displays a clear error message when you try to use them after other components. - Fix 404 error not displaying. 404 responses were missing a content-type header, which made them invisible in the browser. -- Add an `image_url` row-level attribute to the [datagrid](https://sql.ophir.dev/documentation.sql?component=datagrid#component) component to display tiny avatar images in data grids. -- change breakpoints in the [hero](https://sql.ophir.dev/documentation.sql?component=hero#component) component to make it more responsive on middle-sized screens such as tablets or small laptops. This avoids the hero image taking up the whole screen on these devices. -- add an `image_url` row-level attribute to the [list](https://sql.ophir.dev/documentation.sql?component=list#component) component to display small images in lists. +- Add an `image_url` row-level attribute to the [datagrid](https://sql-page.com/documentation.sql?component=datagrid#component) component to display tiny avatar images in data grids. +- change breakpoints in the [hero](https://sql-page.com/documentation.sql?component=hero#component) component to make it more responsive on middle-sized screens such as tablets or small laptops. This avoids the hero image taking up the whole screen on these devices. +- add an `image_url` row-level attribute to the [list](https://sql-page.com/documentation.sql?component=list#component) component to display small images in lists. - Fix bad contrast in links in custom page footers. - Add a new [configuration option](./configuration.md): `environment`. This allows you to set the environment in which SQLPage is running. It can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk when under heavy load. -- Add support for `selected` in multi-select inputs in the [form](https://sql.ophir.dev/documentation.sql?component=form#component) component. This allows you to pre-select some options in a multi-select input. -- New function: [`sqlpage.protocol`](https://sql.ophir.dev/functions.sql?function=protocol#function) to get the protocol used to access the current page. This is useful to build links that point to your own site, and work both in http and https. -- Add an example to the documentation showing how to create heatmaps with the [chart](https://sql.ophir.dev/documentation.sql?component=chart#component) component. +- Add support for `selected` in multi-select inputs in the [form](https://sql-page.com/documentation.sql?component=form#component) component. This allows you to pre-select some options in a multi-select input. +- New function: [`sqlpage.protocol`](https://sql-page.com/functions.sql?function=protocol#function) to get the protocol used to access the current page. This is useful to build links that point to your own site, and work both in http and https. +- Add an example to the documentation showing how to create heatmaps with the [chart](https://sql-page.com/documentation.sql?component=chart#component) component. - 18 new icons available: https://tabler.io/icons/changelog#2.43 -- New top-level attributes for the [`datagrid`](https://sql.ophir.dev/documentation.sql?component=datagrid#component) component: `description`, `description_md` , `icon` , `image_url`. +- New top-level attributes for the [`datagrid`](https://sql-page.com/documentation.sql?component=datagrid#component) component: `description`, `description_md` , `icon` , `image_url`. ## 0.17.0 (2023-11-28) @@ -390,16 +1173,16 @@ select 'form' as component; select 'user_file' as name, 'file' as type; ``` -when received by the server, the file will be saved in a temporary directory (customizable with `TMPDIR` on linux). You can access the temporary file path with the new [`sqlpage.uploaded_file_path`](https://sql.ophir.dev/functions.sql?function=uploaded_file_path#function) function. +when received by the server, the file will be saved in a temporary directory (customizable with `TMPDIR` on linux). You can access the temporary file path with the new [`sqlpage.uploaded_file_path`](https://sql-page.com/functions.sql?function=uploaded_file_path#function) function. -You can then persist the upload as a permanent file on the server with the [`sqlpage.exec`](https://sql.ophir.dev/functions.sql?function=exec#function) function: +You can then persist the upload as a permanent file on the server with the [`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) function: ```sql set file_path = sqlpage.uploaded_file_path('user_file'); select sqlpage.exec('mv', $file_path, '/path/to/my/file'); ``` -or you can store it directly in a database table with the new [`sqlpage.read_file_as_data_url`](https://sql.ophir.dev/functions.sql?function=read_file#function) and [`sqlpage.read_file_as_text`](https://sql.ophir.dev/functions.sql?function=read_file#function) functions: +or you can store it directly in a database table with the new [`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file#function) and [`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file#function) functions: ```sql insert into files (content) values (sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path('user_file'))) @@ -449,8 +1232,8 @@ select upper(name), cast(email as int) from csv_import; ##### Handle uploaded files -- [`sqlpage.uploaded_file_path`](https://sql.ophir.dev/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql.ophir.dev/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location. -- [`sqlpage.uploaded_file_mime_type`](https://sql.ophir.dev/functions.sql?function=uploaded_file_name#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it. +- [`sqlpage.uploaded_file_path`](https://sql-page.com/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location. +- [`sqlpage.uploaded_file_mime_type`](https://sql-page.com/functions.sql?function=uploaded_file_name#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it. The new _Image gallery_ example in the official repository shows how to use these functions to create a simple image gallery with user uploads. @@ -459,8 +1242,8 @@ The new _Image gallery_ example in the official repository shows how to use thes These new functions are useful to read the content of a file uploaded by the user, but can also be used to read any file on the server. -- [`sqlpage.read_file_as_text`](https://sql.ophir.dev/functions.sql?function=read_file#function) reads the contents of a file on the server and returns a text string. -- [`sqlpage.read_file_as_data_url`](https://sql.ophir.dev/functions.sql?function=read_file#function) reads the contents of a file on the server and returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful to embed images directly in web pages, or make link +- [`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file#function) reads the contents of a file on the server and returns a text string. +- [`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file#function) reads the contents of a file on the server and returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful to embed images directly in web pages, or make link ### HTTPS @@ -532,7 +1315,7 @@ and to create JSON APIs. ## 0.16.0 (2023-11-19) -- Add special handling of hidden inputs in [forms](https://sql.ophir.dev/documentation.sql?component=form#component). Hidden inputs are now completely invisible to the end user, facilitating the implementation of multi-step forms, csrf protaction, and other complex forms. +- Add special handling of hidden inputs in [forms](https://sql-page.com/documentation.sql?component=form#component). Hidden inputs are now completely invisible to the end user, facilitating the implementation of multi-step forms, csrf protaction, and other complex forms. - 36 new icons available - https://github.com/tabler/tabler-icons/releases/tag/v2.40.0 - https://github.com/tabler/tabler-icons/releases/tag/v2.41.0 @@ -588,24 +1371,24 @@ and to create JSON APIs. ## 0.15.1 (2023-11-07) -- Many improvements in the [`form`](https://sql.ophir.dev/documentation.sql?component=form#component) component +- Many improvements in the [`form`](https://sql-page.com/documentation.sql?component=form#component) component - Multiple form fields can now be aligned on the same line using the `width` attribute. - A _reset_ button can now be added to the form using the `reset` top-level attribute. - The _submit_ button can now be customized, and can be removed completely, which is useful to create multiple submit buttons that submit the form to different targets. - Support non-string values in markdown fields. `NULL` values are now displayed as empty strings, numeric values are displayed as strings, booleans as `true` or `false`, and arrays as lines of text. This avoids the need to cast values to strings in SQL queries. - Revert a change introduced in v0.15.0: - Re-add the systematic `CAST(? AS TEXT)` around variables, which helps the database know which type it is dealing with in advance. This fixes a regression in 0.15 where some SQLite websites were broken because of missing affinity information. In SQLite `SELECT '1' = 1` returns `false` but `SELECT CAST('1' AS TEXT) = 1` returns `true`. This also fixes error messages like `could not determine data type of parameter $1` in PostgreSQL. -- Fix a bug where [cookie](https://sql.ophir.dev/documentation.sql?component=cookie#component) removal set the cookie value to the empty string instead of removing the cookie completely. -- Support form submission using the [button](https://sql.ophir.dev/documentation.sql?component=button#component) component using its new `form` property. This allows you to create a form with multiple submit buttons that submit the form to different targets. -- Custom icons and colors for markers in the [map](https://sql.ophir.dev/documentation.sql?component=map#component) component. -- Add support for GeoJSON in the [map](https://sql.ophir.dev/documentation.sql?component=map#component) component. This makes it much more generic and allows you to display any kind of geographic data, including areas, on a map very easily. This plays nicely with PostGIS and Spatialite which can return GeoJSON directly from SQL queries. +- Fix a bug where [cookie](https://sql-page.com/documentation.sql?component=cookie#component) removal set the cookie value to the empty string instead of removing the cookie completely. +- Support form submission using the [button](https://sql-page.com/documentation.sql?component=button#component) component using its new `form` property. This allows you to create a form with multiple submit buttons that submit the form to different targets. +- Custom icons and colors for markers in the [map](https://sql-page.com/documentation.sql?component=map#component) component. +- Add support for GeoJSON in the [map](https://sql-page.com/documentation.sql?component=map#component) component. This makes it much more generic and allows you to display any kind of geographic data, including areas, on a map very easily. This plays nicely with PostGIS and Spatialite which can return GeoJSON directly from SQL queries. ## 0.15.0 (2023-10-29) -- New function: [`sqlpage.path`](https://sql.ophir.dev/functions.sql?function=path#function) to get the path of the current page. -- Add a new `align_right` attribute to the [table](https://sql.ophir.dev/documentation.sql?component=table#component) component to align a column to the right. +- New function: [`sqlpage.path`](https://sql-page.com/functions.sql?function=path#function) to get the path of the current page. +- Add a new `align_right` attribute to the [table](https://sql-page.com/documentation.sql?component=table#component) component to align a column to the right. - Fix display of long titles in the shell component. -- New [`sqlpage.variables`](https://sql.ophir.dev/functions.sql?function=variables#function) function for easy handling of complex forms +- New [`sqlpage.variables`](https://sql-page.com/functions.sql?function=variables#function) function for easy handling of complex forms - `sqlpage.variables('get')` returns a json object containing all url parameters. Inside `/my_page.sql?x=1&y=2`, it returns the string `'{"x":"1","y":"2"}'` - `sqlpage.variables('post')` returns a json object containg all variables passed through a form. This makes it much easier to handle a form with a variable number of fields. - Remove systematic casting in SQL of all parameters to `TEXT`. The supported databases understand the type of the parameters natively. @@ -613,12 +1396,12 @@ and to create JSON APIs. ## 0.14.0 (2023-10-19) -- Better support for time series in the [chart](https://sql.ophir.dev/documentation.sql?component=chart#component) component. You can now use the `time` top-attribute to display a time series chart +- Better support for time series in the [chart](https://sql-page.com/documentation.sql?component=chart#component) component. You can now use the `time` top-attribute to display a time series chart with smart x-axis labels. -- **New component**: [button](https://sql.ophir.dev/documentation.sql?component=button#component). This allows you to create rows of buttons that allow navigation between pages. +- **New component**: [button](https://sql-page.com/documentation.sql?component=button#component). This allows you to create rows of buttons that allow navigation between pages. - Better error messages for Microsoft SQL Server. SQLPage now displays the line number of the error, which is especially useful for debugging long migration scripts. - Many improvements in the official website and the documentation. - - Most notably, the documentation now has syntax highlighting on code blocks (using [prism](https://prismjs.com/) with a custom theme made for tabler). This also illustrates the usage of external javascript and css libraries in SQLPage. See [the shell component documentation](https://sql.ophir.dev/documentation.sql?component=shell#component). + - Most notably, the documentation now has syntax highlighting on code blocks (using [prism](https://prismjs.com/) with a custom theme made for tabler). This also illustrates the usage of external javascript and css libraries in SQLPage. See [the shell component documentation](https://sql-page.com/documentation.sql?component=shell#component). - Better display of example queries in the documentation, with smart indentation that makes it easier to read. - Clarify some ambiguous error messages: - make it clearer whether the error comes from SQLPage or from the database @@ -626,11 +1409,11 @@ and to create JSON APIs. ## 0.13.0 (2023-10-16) -- New [timeline](https://sql.ophir.dev/documentation.sql?component=timeline#component) component to display a timeline of events. -- Add support for scatter and bubble plots in the chart component. See [the chart documentation](https://sql.ophir.dev/documentation.sql?component=chart#component). +- New [timeline](https://sql-page.com/documentation.sql?component=timeline#component) component to display a timeline of events. +- Add support for scatter and bubble plots in the chart component. See [the chart documentation](https://sql-page.com/documentation.sql?component=chart#component). - further improve debuggability with more precise error messages. In particular, it usd to be hard to debug errors in long migration scripts, because the line number and position was not displayed. This is now fixed. - Better logs on 404 errors. SQLPage used to log a message without the path of the file that was not found. This made it hard to debug 404 errors. This is now fixed. -- Add a new `top_image` attribute to the [card](https://sql.ophir.dev/documentation.sql?component=card#component) component to display an image at the top of the card. This makes it possible to create beautiful image galleries with SQLPage. +- Add a new `top_image` attribute to the [card](https://sql-page.com/documentation.sql?component=card#component) component to display an image at the top of the card. This makes it possible to create beautiful image galleries with SQLPage. - Updated dependencies, for bug fixes and performance improvements. - New icons (see https://tabler-icons.io/changelog) - When `NULL` is passed as an icon name, display no icon instead of raising an error. @@ -654,7 +1437,7 @@ and to create JSON APIs. - _asynchronous password hashing_ . SQLPage used to block a request processing thread while hashing passwords. This could cause a denial of service if an attacker sent many requests to a page that used `sqlpage.hash_password()` (typically, the account creation page of your website). SQLPage now launches password hashing operations on a separate thread pool, and can continue processing other requests while waiting for passwords to be hashed. -- Easier configuration for multiple menu items. Syntax like `SELECT 'shell' as component, '["page 1", "page 2"]' as menu_item'` now works as expected. See the new `sqlpage_shell` definition in [the small sql game example](./examples/corporate-conundrum/) and [this discussion](https://github.com/lovasoa/SQLpage/discussions/91). +- Easier configuration for multiple menu items. Syntax like `SELECT 'shell' as component, '["page 1", "page 2"]' as menu_item'` now works as expected. See the new `sqlpage_shell` definition in [the small sql game example](./examples/corporate-conundrum/) and [this discussion](https://github.com/sqlpage/SQLPage/discussions/91). - New `sqlpage.exec` function to execute a command on the server. This allows you to run arbitrary code on the server, and use the result in your SQL queries. This can be used to make external API calls, send emails, or run any other code on the server. ```sql @@ -679,7 +1462,7 @@ This function is disabled by default for security reasons. To enable it, set the - Better error messages. SQLPage displays a more precise and useful message when an error occurs, and displays the position in the SQL statement where the error occured. Incorrect error messages on invalid migrations are also fixed. - We now distribute docker images from ARM too. Say hello to SQLPage on your Raspberry Pi and your Mac M1 ! - Create the default SQLite database file in the "sqlpage" config directory instead of at the root of the web server by default. This makes it inaccessible from the web, which is a more secure default. If you want to keep the old behavior, set the `database_url` configuration parameter to `sqlite://sqlpage.db` in your [configuration](./configuration.md). -- New `empty_title`, `empty_description`, and `empty_link` top-level attributes on the [`list`](https://sql.ophir.dev/documentation.sql?component=list#component) component to customize the text displayed when the list is empty. +- New `empty_title`, `empty_description`, and `empty_link` top-level attributes on the [`list`](https://sql-page.com/documentation.sql?component=list#component) component to customize the text displayed when the list is empty. ## 0.11.0 (2023-09-17) @@ -713,7 +1496,7 @@ This function is disabled by default for security reasons. To enable it, set the - Update dashmap for better file lookup performance. - Fix table sorting. - Fix a bug with Basic Authentication. - See [#72](https://github.com/lovasoa/SQLpage/pull/72). Thanks to @edgrip for the contribution ! + See [#72](https://github.com/sqlpage/SQLPage/pull/72). Thanks to @edgrip for the contribution ! ## 0.10.0 (2023-08-20) @@ -734,11 +1517,11 @@ This function is disabled by default for security reasons. To enable it, set the ```sql SELECT 'shell' AS component, 'dark' AS theme; ``` - See https://github.com/lovasoa/SQLpage/issues/50 + See https://github.com/sqlpage/SQLPage/issues/50 - Fixed a bug where the default index page would be displayed when `index.sql` could not be loaded, instead of displaying an error page explaining the issue. - Improved the appearance of scrollbars. (Workaround for https://github.com/tabler/tabler/issues/1648). - See https://github.com/lovasoa/SQLpage/discussions/17 + See https://github.com/sqlpage/SQLPage/discussions/17 - Create a single database connection by default when using `sqlite://:memory:` as the database URL. This makes it easier to use temporary tables and other connection-specific features. - When no component is selected, display data with the `debug` component by default. @@ -752,7 +1535,7 @@ This function is disabled by default for security reasons. To enable it, set the ## 0.9.5 (2023-08-12) -- New `tab` component to create tabbed interfaces. See [the documentation](https://sql.ophir.dev/documentation.sql?component=tab#component). +- New `tab` component to create tabbed interfaces. See [the documentation](https://sql-page.com/documentation.sql?component=tab#component). - Many improvements in database drivers. - performance and numeric precision improvements, - multiple fixes around passing NUMERIC, DECIMAL, and JSON values to SQLPage. @@ -773,12 +1556,12 @@ Small bugfix release - Icons are now loaded directly from the sqlpage binary instead of loading them from a CDN. This allows pages to load faster, and to get a better score on google's performance audits, potentially improving your position in search results. - This also makes it possible to host a SQLPage website on an intranet without access to the internet. - - Fixes https://github.com/lovasoa/SQLpage/issues/37 + - Fixes https://github.com/sqlpage/SQLPage/issues/37 - store compressed frontend assets in the SQLPage binary: - smaller SQLPage binary - Faster page loads, less work on the server - Fix a bug where table search would fail to find a row if the search term contained some special characters. - - Fixes https://github.com/lovasoa/SQLpage/issues/46 + - Fixes https://github.com/sqlpage/SQLPage/issues/46 - Split the charts javascript code from the rest of the frontend code, and load it only when necessary. This greatly diminishes the amount of js loaded by default, and achieves very good performance scores by default. SQLPage websites now load even faster, een on slow mobile connections. @@ -786,7 +1569,7 @@ Small bugfix release ## 0.9.2 (2023-08-01) - Added support for more SQL data types. This notably fixes an issue with the display of datetime columns in tables. - - See: https://github.com/lovasoa/SQLpage/issues/41 + - See: https://github.com/sqlpage/SQLPage/issues/41 - Updated dependencies, better SQL drivers ## 0.9.1 (2023-07-30) @@ -811,37 +1594,37 @@ Small bugfix release - querying CSV data from SQLPage with [vsv](https://github.com/nalgeon/sqlean/blob/main/docs/vsv.md), - or building a search engine for your data with [FTS5](https://www.sqlite.org/fts5.html). - Breaking: change the order of priority for loading configuration parameters: the environment variables have priority over the configuration file. This makes it easier to tweak the configuration of a SQLPage website when deploying it. -- Fix the default index page in MySQL. Fixes [#23](https://github.com/lovasoa/SQLpage/issues/23). -- Add a new [map](https://sql.ophir.dev/documentation.sql?component=map#component) component to display a map with markers on it. Useful to display geographic data from PostGIS or Spatialite. -- Add a new `icon` attribute to the [table](https://sql.ophir.dev/documentation.sql?component=table#component) component to display icons in the table. -- Fix `textarea` fields in the [form](https://sql.ophir.dev/documentation.sql?component=table#component) component to display the provided `value` attribute. Thanks Frank for the contribution ! +- Fix the default index page in MySQL. Fixes [#23](https://github.com/sqlpage/SQLPage/issues/23). +- Add a new [map](https://sql-page.com/documentation.sql?component=map#component) component to display a map with markers on it. Useful to display geographic data from PostGIS or Spatialite. +- Add a new `icon` attribute to the [table](https://sql-page.com/documentation.sql?component=table#component) component to display icons in the table. +- Fix `textarea` fields in the [form](https://sql-page.com/documentation.sql?component=table#component) component to display the provided `value` attribute. Thanks Frank for the contribution ! - SQLPage now guarantees that a single web request will be handled by a single database connection. Previously, connections were repeatedly taken and put back to the connection pool between each statement, preventing the use of temporary tables, transactions, and other connection-specific features such as [`last_insert_rowid`](https://www.sqlite.org/lang_corefunc.html#last_insert_rowid). This makes it much easier to keep state between SQL statements in a single `.sql` file. Please report any performance regression you might encounter. See [the many-to-many relationship example](./examples/modeling%20a%20many%20to%20many%20relationship%20with%20a%20form/). -- The [table](https://sql.ophir.dev/documentation.sql?component=table#component) component now supports setting a custom background color, and a custom CSS class on a given table line. +- The [table](https://sql-page.com/documentation.sql?component=table#component) component now supports setting a custom background color, and a custom CSS class on a given table line. - New `checked` attribute for checkboxes and radio buttons. ## 0.7.2 (2023-07-10) -### [SQL components](https://sql.ophir.dev/documentation.sql) +### [SQL components](https://sql-page.com/documentation.sql) -- New [authentication](https://sql.ophir.dev/documentation.sql?component=authentication#component) component to handle user authentication, and password checking -- New [redirect](https://sql.ophir.dev/documentation.sql?component=redirect#component) component to stop rendering the current page and redirect the user to another page. -- The [debug](https://sql.ophir.dev/documentation.sql?component=debug#component) component is now documented -- Added properties to the [shell](https://sql.ophir.dev/documentation.sql?component=shell#component) component: +- New [authentication](https://sql-page.com/documentation.sql?component=authentication#component) component to handle user authentication, and password checking +- New [redirect](https://sql-page.com/documentation.sql?component=redirect#component) component to stop rendering the current page and redirect the user to another page. +- The [debug](https://sql-page.com/documentation.sql?component=debug#component) component is now documented +- Added properties to the [shell](https://sql-page.com/documentation.sql?component=shell#component) component: - `css` to add custom CSS to the page - - `javascript` to add custom Javascript to the page. An example of [how to use it to integrate a react component](https://github.com/lovasoa/SQLpage/tree/main/examples/using%20react%20and%20other%20custom%20scripts%20and%20styles) is available. + - `javascript` to add custom Javascript to the page. An example of [how to use it to integrate a react component](https://github.com/sqlpage/SQLPage/tree/main/examples/using%20react%20and%20other%20custom%20scripts%20and%20styles) is available. - `footer` to set a message in the footer of the page -### [SQLPage functions](https://sql.ophir.dev/functions.sql) +### [SQLPage functions](https://sql-page.com/functions.sql) -- New [`sqlpage.basic_auth_username`](https://sql.ophir.dev/functions.sql?function=basic_auth_username#function) function to get the name of the user logged in with HTTP basic authentication -- New [`sqlpage.basic_auth_password`](https://sql.ophir.dev/functions.sql?function=basic_auth_password#function) function to get the password of the user logged in with HTTP basic authentication. -- New [`sqlpage.hash_password`](https://sql.ophir.dev/functions.sql?function=hash_password#function) function to hash a password with the same algorithm as the [authentication](https://sql.ophir.dev/documentation.sql?component=authentication#component) component uses. -- New [`sqlpage.header`](https://sql.ophir.dev/functions.sql?function=header#function) function to read an HTTP header from the request. -- New [`sqlpage.random_string`](https://sql.ophir.dev/functions.sql?function=random_string#function) function to generate a random string. Useful to generate session ids. +- New [`sqlpage.basic_auth_username`](https://sql-page.com/functions.sql?function=basic_auth_username#function) function to get the name of the user logged in with HTTP basic authentication +- New [`sqlpage.basic_auth_password`](https://sql-page.com/functions.sql?function=basic_auth_password#function) function to get the password of the user logged in with HTTP basic authentication. +- New [`sqlpage.hash_password`](https://sql-page.com/functions.sql?function=hash_password#function) function to hash a password with the same algorithm as the [authentication](https://sql-page.com/documentation.sql?component=authentication#component) component uses. +- New [`sqlpage.header`](https://sql-page.com/functions.sql?function=header#function) function to read an HTTP header from the request. +- New [`sqlpage.random_string`](https://sql-page.com/functions.sql?function=random_string#function) function to generate a random string. Useful to generate session ids. ### Bug fixes -- Fix a bug where the page style would not load in pages that were not in the root directory: https://github.com/lovasoa/SQLpage/issues/19 +- Fix a bug where the page style would not load in pages that were not in the root directory: https://github.com/sqlpage/SQLPage/issues/19 - Fix resources being served with the wrong content type - Fix compilation of SQLPage as an AWS lambda function - Fixed logging and display of errors, to make them more useful diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e6ad47db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,232 @@ +# Contributing to SQLPage + +Thank you for your interest in contributing to SQLPage! This document will guide you through the contribution process. + +## Development Setup + +1. Install Rust and Cargo (latest stable version): https://www.rust-lang.org/tools/install +2. If you contribute to the frontend, install Node.js too for frontend tooling: https://nodejs.org/en/download/ +3. Clone the repository + +```bash +git clone https://github.com/sqlpage/sqlpage +cd sqlpage +``` + +## Building the project + +The first time you build the project, +dependencies will be downloaded, so you will need internet access, +and the build may take a while. + +Run the following command from the root of the repository to build the project in development mode: + +```bash +cargo build +``` + +The resulting executable will be in `target/debug/sqlpage`. + +### Release mode + +To build the project in release mode: + +```bash +cargo build --release +``` + +The resulting executable will be in `target/release/sqlpage`. + +### ODBC build modes + +SQLPage can either be built with an integrated odbc driver manager (static linking), +or depend on having one already installed on the system where it is running (dynamic linking). + +- Dynamic ODBC (default): `cargo build` +- Static ODBC (Linux and MacOS only): `cargo build --features odbc-static` + +Windows comes with ODBC pre-installed; SQLPage cannot statically link to the unixODBC driver manager on windows. + +## Code Style and Linting + +### Rust + +- Use `cargo fmt` to format your Rust code +- Run `cargo clippy` to catch common mistakes and improve code quality +- All code must pass the following checks: + +```bash +cargo fmt --all -- --check +cargo clippy +``` + +### Frontend + +We use Biome for linting and formatting of the frontend code. + +```bash +npx @biomejs/biome check . +``` + +This will check the entire codebase (html, css, js). + +## Testing + +### Rust Tests + +Run the backend tests: + +```bash +cargo test +``` + +By default, the tests are run against an SQLite in-memory database. + +If you want to run them against another database, +start a database server with `docker compose up database_name` (mssql, mysql, mariadb, or postgres) +and run the tests with the `DATABASE_URL` environment variable pointing to the database: + +```bash +docker compose up mssql # or mysql, mariadb, postgres +export DATABASE_URL=mssql://root:Password123!@localhost/sqlpage +cargo test +``` + +### End-to-End Tests + +We use Playwright for end-to-end testing of dynamic frontend features. +Tests are located in [`tests/end-to-end/`](./tests/end-to-end/). Key areas covered include: + +#### Start a sqlpage instance pointed to the official site source code + +```bash +cd examples/official-site +cargo run +``` + +#### Run the tests + +In a separate terminal, run the tests: + +```bash +cd tests/end-to-end +npm install +npm run test +``` + +## Documentation + +### Component Documentation + +When adding new components, comprehensive documentation is required. Example from a component documentation: + +```sql +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('component_name', 'icon_name', 'Description of the component', 'version'); + +-- Document all parameters +INSERT INTO parameter(component, name, description, type, top_level, optional) +VALUES ('component_name', 'param_name', 'param_description', 'TEXT|BOOLEAN|NUMBER|JSON|ICON|COLOR', false, true); + +-- Include usage examples +INSERT INTO example(component, description, properties) VALUES + ('component_name', 'Example description in markdown', JSON('[ +{"component": "new_component_name", "top_level_property_1": "value1", "top_level_property_2": "value2"}, +{"row_level_property_1": "value1", "row_level_property_2": "value2"} +]')); +``` + +Component documentation is stored in [`./examples/official-site/sqlpage/migrations/`](./examples/official-site/sqlpage/migrations/). + +If you are editing an existing component, edit the existing sql documentation file directly. +If you are adding a new component, add a new sql file in the folder, and add the appropriate insert statements above. + +### SQLPage Function Documentation + +When adding new SQLPage functions, document them using a SQL migrations. Example structure: + +```sql +-- Function Definition +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" +) +VALUES ( + 'your_function_name', + '1.0.0', + 'function-icon-name', + 'Description of what the function does. + +### Example + + select ''text'' as component, sqlpage.your_function_name(''parameter'') as result; + +Additional markdown documentation, usage notes, and examples go here. +'); + +-- Function Parameters +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" +) +VALUES ( + 'your_function_name', + 1, + 'parameter_name', + 'Description of what this parameter does and how to use it.', + 'TEXT|BOOLEAN|NUMBER|JSON' +); +``` + +Key elements to include in function documentation: + +- Clear description of the function's purpose +- Version number where the function was introduced +- Appropriate icon +- Markdown-formatted documentation with examples +- All parameters documented with clear descriptions and types +- Security considerations if applicable +- Example usage scenarios + +## Pull Request Process + +1. Create a new branch for your feature/fix: + +```bash +git checkout -b feature/your-feature-name +``` + +2. Make your changes, ensuring: + +- All tests pass +- Code is properly formatted +- New features are documented +- tests cover new functionality + +3. Push your changes and create a Pull Request + +4. CI Checks + Our CI pipeline will automatically: + - Run Rust formatting and clippy checks + - Execute all tests across multiple platforms (Linux, Windows) + - Build Docker images for multiple architectures + - Run frontend linting with Biome + - Test against multiple databases (PostgreSQL, MySQL, MSSQL) + +## Release Process + +Releases are automated when pushing tags that match the pattern `v*` (e.g., `v1.0.0`). The CI pipeline will: + +- Build and test the code +- Create Docker images for multiple architectures +- Push images to Docker Hub +- Create GitHub releases + +## Questions? + +If you have any questions, feel free to open an issue or discussion on GitHub. diff --git a/Cargo.lock b/Cargo.lock index 23158d6b..5a97b48a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", @@ -21,24 +21,24 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.9.0" +version = "3.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-tls", "actix-utils", - "ahash", "base64 0.22.1", - "bitflags 2.6.0", - "brotli 6.0.0", + "bitflags 2.10.0", + "brotli 8.0.2", "bytes", "bytestring", - "derive_more", + "derive_more 2.1.1", "encoding_rs", "flate2", + "foldhash 0.1.5", "futures-core", "h2", "http 0.2.12", @@ -50,7 +50,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.9.2", "sha1", "smallvec", "tokio", @@ -66,7 +66,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.74", + "syn", ] [[package]] @@ -78,7 +78,7 @@ dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "derive_more", + "derive_more 0.99.20", "futures-core", "futures-util", "httparse", @@ -86,7 +86,7 @@ dependencies = [ "log", "memchr", "mime", - "rand", + "rand 0.8.5", "serde", "serde_json", "serde_plain", @@ -100,11 +100,11 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" dependencies = [ - "darling", + "darling 0.20.11", "parse-size", "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ "actix-macros", "futures-core", @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ "actix-rt", "actix-service", @@ -145,34 +145,33 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2", + "socket2 0.5.10", "tokio", "tracing", ] [[package]] name = "actix-service" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" dependencies = [ "futures-core", - "paste", "pin-project-lite", ] [[package]] name = "actix-tls" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "http 0.2.12", - "http 1.1.0", + "http 1.4.0", "impl-more", "pin-project-lite", "rustls-pki-types", @@ -180,7 +179,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tracing", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] @@ -195,9 +194,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.9.0" +version = "4.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" dependencies = [ "actix-codec", "actix-http", @@ -209,13 +208,13 @@ dependencies = [ "actix-tls", "actix-utils", "actix-web-codegen", - "ahash", "bytes", "bytestring", "cfg-if", "cookie", - "derive_more", + "derive_more 2.1.1", "encoding_rs", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -231,8 +230,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.6.1", "time", + "tracing", "url", ] @@ -245,7 +245,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] @@ -264,19 +264,10 @@ dependencies = [ ] [[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adler32" @@ -286,12 +277,12 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -299,9 +290,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -323,15 +314,36 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" [[package]] name = "android_system_properties" @@ -344,9 +356,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -359,43 +371,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "argon2" @@ -409,11 +422,17 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "asn1-rs" -version = "0.5.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -421,38 +440,38 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] [[package]] name = "asn1-rs-derive" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", "synstructure", ] [[package]] name = "asn1-rs-impl" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -468,39 +487,27 @@ checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91" dependencies = [ "anyhow", "futures", - "http 1.1.0", + "http 1.4.0", "httparse", "log", ] [[package]] name = "async-io" -version = "2.3.4" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.3", "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.3.1", - "event-listener-strategy", - "pin-project-lite", + "windows-sys 0.61.2", ] [[package]] @@ -522,14 +529,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -538,13 +545,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] @@ -555,36 +562,31 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "async-web-client" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a820ef79f63962244fc33d3f17dafb41f1c6bb9754de97c3c09c57c1b8a360ce" +checksum = "37381fb4fad3cd9b579628c21a58f528ef029d1f072d10f16cb9431aa2236d29" dependencies = [ "async-http-codec", "async-net", "futures", "futures-rustls", - "gloo-net", - "http 1.1.0", - "js-sys", + "http 1.4.0", "lazy_static", "log", "rustls-pki-types", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", + "thiserror 1.0.69", + "webpki-roots 0.26.11", ] [[package]] @@ -604,15 +606,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "awc" -version = "3.5.1" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79049b2461279b886e46f1107efc347ebecc7b88d74d023dda010551a124967b" +checksum = "3c170039c11c7f6c0a28f7b3bd4fb0c674cbfa317fabc1560022ad3ec2d69e7c" dependencies = [ "actix-codec", "actix-http", @@ -624,7 +626,7 @@ dependencies = [ "bytes", "cfg-if", "cookie", - "derive_more", + "derive_more 2.1.1", "futures-core", "futures-util", "h2", @@ -634,7 +636,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.9.2", "rustls", "serde", "serde_json", @@ -643,20 +645,33 @@ dependencies = [ ] [[package]] -name = "backtrace" -version = "0.3.73" +name = "aws-lc-rs" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" dependencies = [ - "addr2line", "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", + "cmake", + "dunce", + "fs_extra", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -677,9 +692,24 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bigdecimal" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", + "serde_json", +] [[package]] name = "bitflags" @@ -689,11 +719,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -714,11 +744,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -740,13 +779,13 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor 4.0.1", + "brotli-decompressor 5.0.0", ] [[package]] @@ -761,9 +800,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -771,9 +810,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -783,54 +822,141 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bytestring" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" dependencies = [ "bytes", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + [[package]] name = "cc" -version = "1.1.10" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ + "find-msvc-tools", "jobserver", "libc", + "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", ] [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "concurrent-queue" @@ -843,22 +969,22 @@ dependencies = [ [[package]] name = "config" -version = "0.14.0" +version = "0.15.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" dependencies = [ "async-trait", "convert_case 0.6.0", "json5", - "lazy_static", - "nom", "pathdiff", "ron", "rust-ini", - "serde", + "serde-untagged", + "serde_core", "serde_json", "toml", - "yaml-rust", + "winnow", + "yaml-rust2", ] [[package]] @@ -882,7 +1008,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -902,6 +1028,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -923,12 +1058,46 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core2" version = "0.4.0" @@ -940,18 +1109,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -964,33 +1133,45 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" @@ -1004,9 +1185,9 @@ dependencies = [ [[package]] name = "csv-async" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37fe5b0d07f4a8260ce1e9a81413e88f459af0f2dfc55c15e96868a2f99c0f0" +checksum = "888dbb0f640d2c4c04e50f933885c7e9c95995d93cec90aba8735b4c610f26f1" dependencies = [ "cfg-if", "csv-core", @@ -1020,76 +1201,144 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] -name = "darling" -version = "0.20.10" +name = "cursor-icon" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] -name = "darling_core" -version = "0.20.10" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.74", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "darling_macro" -version = "0.20.10" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "darling_core", + "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] -name = "dary_heap" -version = "0.3.6" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] [[package]] -name = "data-encoding" -version = "2.6.0" +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] [[package]] -name = "der" -version = "0.7.9" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] -name = "der-parser" -version = "8.2.0" +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ "asn1-rs", "displaydoc", @@ -1101,24 +1350,79 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", ] [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 2.0.74", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", ] [[package]] @@ -1135,25 +1439,31 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.5" @@ -1162,7 +1472,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", ] [[package]] @@ -1180,26 +1499,103 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -1207,49 +1603,49 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "errno" -version = "0.3.9" +name = "erased-serde" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ - "libc", - "windows-sys 0.52.0", + "serde", + "serde_core", + "typeid", ] [[package]] -name = "event-listener" -version = "3.1.0" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "libc", + "windows-sys 0.61.2", ] [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1258,25 +1654,47 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.3.1", + "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" -version = "1.0.31" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1284,9 +1702,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -1299,20 +1717,65 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1325,9 +1788,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1335,15 +1798,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1363,15 +1826,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1382,20 +1845,20 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "futures-rustls" -version = "0.25.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", "rustls", @@ -1404,21 +1867,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1434,61 +1897,56 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "gloo-net" -version = "0.2.6" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "gloo-utils", - "js-sys", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "cfg-if", + "libc", + "r-efi", + "wasip2", ] [[package]] -name = "gloo-utils" -version = "0.1.7" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -1496,7 +1954,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -1505,41 +1963,59 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.0.0" +version = "6.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5226a0e122dc74917f3a701484482bed3ee86d016c7356836abbaa033133a157" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" dependencies = [ + "derive_builder", "log", + "num-order", "pest", "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -1550,15 +2026,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1597,12 +2067,11 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1619,9 +2088,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1629,17 +2098,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1651,7 +2114,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1660,14 +2123,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1682,26 +2146,118 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" -version = "0.5.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "impl-more" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "include_dir" @@ -1724,41 +2280,111 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1839,15 +2465,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libflate" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" +checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74" dependencies = [ "adler32", "core2", @@ -1858,36 +2484,47 @@ dependencies = [ [[package]] name = "libflate_lz77" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" +checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c" dependencies = [ "core2", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "rle-decode-fast", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "libc", + "redox_syscall 0.7.0", ] [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -1895,16 +2532,22 @@ dependencies = [ ] [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "linux-raw-sys" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-channel" @@ -1925,25 +2568,24 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "markdown" -version = "1.0.0-alpha.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a8325e6fb87b89890cd4529a2ab34c2669c026279e61c26b7140a3d821ccb" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" dependencies = [ "log", "unicode-id", @@ -1961,9 +2603,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1989,24 +2631,54 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "hermit-abi 0.3.9", "libc", "log", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", ] [[package]] @@ -2031,17 +2703,16 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2072,6 +2743,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2083,207 +2769,548 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.3" +name = "num_enum" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "memchr", + "num_enum_derive", + "rustversion", ] [[package]] -name = "oid-registry" -version = "0.6.1" +name = "num_enum_derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "asn1-rs", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "openssl-probe" -version = "0.1.5" +name = "oauth2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.16", + "http 1.4.0", + "rand 0.8.5", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] [[package]] -name = "option-ext" -version = "0.2.0" +name = "objc-sys" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] -name = "ordered-multimap" -version = "0.6.0" +name = "objc2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "dlv-list", - "hashbrown 0.13.2", + "objc-sys", + "objc2-encode", ] [[package]] -name = "parking" -version = "2.2.0" +name = "objc2-app-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "objc2-cloud-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "lock_api", - "parking_lot_core", + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "objc2-contacts" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.3", - "smallvec", - "windows-targets 0.52.6", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "parse-size" -version = "1.0.0" +name = "objc2-core-data" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", +] [[package]] -name = "password-hash" -version = "0.5.0" +name = "objc2-core-image" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "base64ct", - "rand_core", - "subtle", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] -name = "paste" -version = "1.0.15" +name = "objc2-core-location" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] [[package]] -name = "pathdiff" -version = "0.2.1" +name = "objc2-encode" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] -name = "pem" -version = "3.0.4" +name = "objc2-foundation" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "base64 0.22.1", - "serde", + "bitflags 2.10.0", + "block2", + "dispatch", + "libc", + "objc2", ] [[package]] -name = "pem-rfc7468" -version = "0.7.0" +name = "objc2-link-presentation" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "base64ct", + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "objc2-metal" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", +] [[package]] -name = "pest" -version = "2.7.11" +name = "objc2-quartz-core" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "memchr", - "thiserror", - "ucd-trie", + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] -name = "pest_derive" -version = "2.7.11" +name = "objc2-symbols" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "pest", - "pest_generator", + "objc2", + "objc2-foundation", ] [[package]] -name = "pest_generator" -version = "2.7.11" +name = "objc2-ui-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.74", + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", ] [[package]] -name = "pest_meta" -version = "2.7.11" +name = "objc2-uniform-type-identifiers" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "once_cell", - "pest", - "sha2", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "pin-project" -version = "1.1.5" +name = "objc2-user-notifications" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "pin-project-internal", + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] -name = "pin-project-internal" -version = "1.1.5" +name = "odbc-api" +version = "19.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "f017d3949731e436bc1bb9a1fbc34197c2f39c588cdcb60d21adb1f8dd3b8514" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.74", + "atoi", + "log", + "odbc-sys 0.27.4", + "thiserror 2.0.17", + "widestring", + "winit", ] [[package]] -name = "pin-project-lite" -version = "0.2.14" +name = "odbc-sys" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "1896e52e97c2f0cf997cc627380f1af1ecb3f6c29ce6175047cd38adaadb46f5" [[package]] -name = "pin-utils" +name = "odbc-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348f5e1d16a8aa07e9e76fc62f82bf44d94c099c0d291b4b4b7e10574447434c" +dependencies = [ + "unix-odbc", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 1.4.0", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" @@ -2322,23 +3349,46 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "3.7.3" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", ] [[package]] @@ -2349,31 +3399,55 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -2381,8 +3455,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2392,7 +3476,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2401,17 +3495,33 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rcgen" -version = "0.12.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ + "aws-lc-rs", "pem", - "ring", + "rustls-pki-types", "time", "yasna", ] @@ -2427,29 +3537,58 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2459,9 +3598,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2470,27 +3609,36 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2503,21 +3651,23 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "ron" -version = "0.8.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64 0.21.7", - "bitflags 2.6.0", + "bitflags 2.10.0", + "once_cell", "serde", "serde_derive", + "typeid", + "unicode-ident", ] [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -2526,7 +3676,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2535,25 +3685,19 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.19.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -2569,25 +3713,39 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", "log", - "ring", + "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2596,38 +3754,37 @@ dependencies = [ [[package]] name = "rustls-acme" -version = "0.9.2" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f6de93ea3b4a88a9048f753f6db50242d2bd2633d12e06394a3ee41472bbb08" +checksum = "4b49bf42910782ed684d992550c267c98fbe602320d6bb4a6362292791076eed" dependencies = [ "async-io", "async-trait", "async-web-client", - "base64 0.21.7", + "aws-lc-rs", + "base64 0.22.1", "blocking", "chrono", "futures", "futures-rustls", - "http 1.1.0", + "http 1.4.0", "log", "pem", "rcgen", - "ring", "serde", "serde_json", - "thiserror", - "webpki-roots", + "thiserror 2.0.17", + "webpki-roots 1.0.4", "x509-parser", ] [[package]] name = "rustls-native-certs" -version = "0.7.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", @@ -2635,44 +3792,86 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] @@ -2681,14 +3880,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" -version = "2.11.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2696,9 +3909,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2706,41 +3919,85 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.207" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.207" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "serde_json" -version = "1.0.124" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ - "indexmap", + "indexmap 2.12.1", "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", ] [[package]] @@ -2754,11 +4011,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2773,6 +4030,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2786,21 +4074,28 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2811,34 +4106,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -2858,20 +4175,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlpage" -version = "0.27.0" +version = "0.41.0" dependencies = [ + "actix-http", "actix-multipart", "actix-rt", "actix-web", @@ -2883,38 +4191,46 @@ dependencies = [ "async-trait", "awc", "base64 0.22.1", + "bigdecimal", "chrono", + "clap", "config", "csv-async", "dotenvy", + "encoding_rs", "env_logger", "futures-util", "handlebars", + "hmac", "include_dir", "lambda-web", "libflate", "log", "markdown", "mime_guess", + "odbc-sys 0.28.0", + "openidconnect", "password-hash", "percent-encoding", - "rand", + "rand 0.9.2", "rustls", "rustls-acme", "rustls-native-certs", "serde", "serde_json", + "sha2", "sqlparser", "sqlx-oldapi", "tokio", "tokio-stream", + "tokio-util", ] [[package]] name = "sqlparser" -version = "0.49.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a404d0e14905361b918cb8afdb73605e25c1d5029312bd9785142dcb3aa49e" +checksum = "505aa16b045c4c1375bf5f125cce3813d0176325bfe9ffc4a903f423de7774ff" dependencies = [ "log", "sqlparser_derive", @@ -2922,25 +4238,26 @@ dependencies = [ [[package]] name = "sqlparser_derive" -version = "0.2.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" +checksum = "028e551d5e270b31b9f3ea271778d9d827148d4287a5d96167b6bb9787f5cc38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "sqlx-core-oldapi" -version = "0.6.25" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc46f671eaefbe2949e2cb73563265a08952c37157bf57cc3bc245e956af90b5" +checksum = "8b9869b844b6ab5f575c33e29ad579a3c880bc514bb47c4c9991d0dd6979949b" dependencies = [ "ahash", "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bigdecimal", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -2951,7 +4268,7 @@ dependencies = [ "dotenvy", "either", "encoding_rs", - "event-listener 3.1.0", + "event-listener", "flume", "futures-channel", "futures-core", @@ -2962,7 +4279,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "indexmap", + "indexmap 2.12.1", "itoa", "libc", "libsqlite3-sys", @@ -2970,10 +4287,11 @@ dependencies = [ "md-5", "memchr", "num-bigint", + "odbc-api", "once_cell", "paste", "percent-encoding", - "rand", + "rand 0.8.5", "regex", "rsa", "rustls", @@ -2983,22 +4301,22 @@ dependencies = [ "sha1", "sha2", "smallvec", - "sqlformat", "sqlx-rt-oldapi", "stringprep", - "thiserror", + "thiserror 2.0.17", "tokio-stream", + "tokio-util", "url", "uuid", - "webpki-roots", + "webpki-roots 1.0.4", "whoami", ] [[package]] name = "sqlx-macros-oldapi" -version = "0.6.25" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90d70bdff47f20251b30fb5be1e3dd474f8f22bd0e3810ff8a4e1257308caddf" +checksum = "78820a192cc29b877b735c32e1c1a8e51459019b699fff6f5ba86a128fa9ef9d" dependencies = [ "dotenvy", "either", @@ -3010,15 +4328,15 @@ dependencies = [ "sha2", "sqlx-core-oldapi", "sqlx-rt-oldapi", - "syn 1.0.109", + "syn", "url", ] [[package]] name = "sqlx-oldapi" -version = "0.6.25" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38632aaa363be203450d0369c0582f45de53da4b00c428de1a65610b445713bb" +checksum = "1a74816da5fc417f929012d46ca806381dabca75de303b248519aad466844044" dependencies = [ "sqlx-core-oldapi", "sqlx-macros-oldapi", @@ -3026,15 +4344,21 @@ dependencies = [ [[package]] name = "sqlx-rt-oldapi" -version = "0.6.25" +version = "0.6.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d95609d90b95d8037871aa002f85c4df6585c5a2c271a8509ce4cac45b33b8" +checksum = "b9b54748f0bfadc0b3407b4ee576132b4b5ad0730ebec82e0dbec9d0d1a233bc" dependencies = [ "once_cell", "tokio", "tokio-rustls", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stringprep" version = "0.1.5" @@ -3060,9 +4384,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -3070,66 +4394,74 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.74" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "tempfile" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] -name = "tempfile" -version = "3.12.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "1.0.63" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3142,15 +4474,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3165,11 +4497,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3182,49 +4524,47 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3233,12 +4573,13 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -3246,35 +4587,44 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ - "serde", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap", - "serde", - "serde_spanned", + "indexmap 2.12.1", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ "winnow", ] @@ -3307,9 +4657,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3319,20 +4669,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -3343,77 +4693,83 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-id" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "unix-odbc" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "26bdaf2156eebadc0dbabec5b2c2a6f92bff5cface28f3f0a367d2ee9aeca0e2" +dependencies = [ + "cc", +] [[package]] name = "untrusted" @@ -3423,15 +4779,22 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3440,9 +4803,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "vcpkg" @@ -3456,6 +4823,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3467,9 +4844,18 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasite" @@ -3479,47 +4865,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.74", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3527,28 +4901,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.74", - "wasm-bindgen-backend", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3556,40 +4943,114 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall 0.4.1", + "libredox", "wasite", "web-sys", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.42.2", ] [[package]] @@ -3610,19 +5071,37 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3634,18 +5113,35 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -3653,11 +5149,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -3665,11 +5167,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -3677,17 +5185,29 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -3695,11 +5215,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -3707,11 +5233,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -3719,11 +5251,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -3731,20 +5269,78 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "xkbcommon-dl", +] + [[package]] name = "winnow" -version = "0.6.18" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "x509-parser" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ "asn1-rs", "data-encoding", @@ -3753,17 +5349,38 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yaml-rust2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ - "linked-hash-map", + "arraydeque", + "encoding_rs", + "hashlink", ] [[package]] @@ -3775,56 +5392,138 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn", + "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 82e2b18d..044bef3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "sqlpage" -version = "0.27.0" +version = "0.41.0" edition = "2021" description = "Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] license = "MIT" -homepage = "https://sql.ophir.dev/" -repository = "https://github.com/lovasoa/SQLpage" +homepage = "https://sql-page.com/" +repository = "https://github.com/sqlpage/SQLPage" documentation = "https://docs.rs/sqlpage" -include = ["/src", "/README.md", "/build.rs", "/sqlpage", "/index.sql", "/.sqlpage_artefacts"] +include = ["/src", "/README.md", "/build.rs", "/sqlpage"] [profile.superoptimized] inherits = "release" @@ -18,20 +18,24 @@ panic = "abort" codegen-units = 2 [dependencies] -sqlx = { package = "sqlx-oldapi", version = "0.6.25", features = [ +sqlx = { package = "sqlx-oldapi", version = "0.6.51", default-features = false, features = [ "any", - "runtime-actix-rustls", + "runtime-tokio-rustls", + "migrate", "sqlite", "postgres", "mysql", "mssql", + "odbc", "chrono", + "bigdecimal", "json", + "uuid", ] } chrono = "0.4.23" -actix-web = { version = "4", features = ["rustls-0_22", "cookies"] } +actix-web = { version = "4", features = ["rustls-0_23", "cookies"] } percent-encoding = "2.2.0" -handlebars = "6.0.0" +handlebars = "6.2.0" log = "0.4.17" env_logger = "0.11.1" mime_guess = "2.0.4" @@ -40,30 +44,55 @@ tokio = { version = "1.24.1", features = ["macros", "rt", "process", "sync"] } tokio-stream = "0.1.9" anyhow = "1" serde = "1" -serde_json = { version = "1.0.82", features = ["preserve_order", "raw_value"] } +serde_json = { version = "1.0.82", features = [ + "preserve_order", + "raw_value", + "arbitrary_precision", +] } lambda-web = { version = "0.2.1", features = ["actix4"], optional = true } -sqlparser = { version = "0.49.0", features = ["visitor"] } +sqlparser = { version = "0.60.0", default-features = false, features = [ + "std", + "visitor", +] } async-stream = "0.3" async-trait = "0.1.61" async-recursion = "1.0.0" +bigdecimal = { version = "0.4.8", features = ["serde-json"] } include_dir = "0.7.2" -config = { version = "0.14.0", features = ["json"] } -markdown = { version = "1.0.0-alpha.15", features = ["log"] } +config = { version = "0.15.4", features = ["json"] } +markdown = { version = "1.0.0-alpha.23", features = ["log"] } password-hash = "0.5.0" -argon2 = "0.5.0" +argon2 = "0.5.3" actix-web-httpauth = "0.8.0" -rand = "0.8.5" +rand = "0.9.0" actix-multipart = "0.7.2" base64 = "0.22" -rustls-acme = "0.9.2" +hmac = "0.12" +sha2 = "0.10" +rustls-acme = "0.14" dotenvy = "0.15.7" csv-async = { version = "1.2.6", features = ["tokio"] } -rustls = { version = "0.22.0" } # keep in sync with actix-web, awc, rustls-acme, and sqlx -rustls-native-certs = "0.7.0" -awc = { version = "3", features = ["rustls-0_22-webpki-roots"] } +rustls = { version = "0.23" } # keep in sync with actix-web, awc, rustls-acme, and sqlx +rustls-native-certs = "0.8.1" +awc = { version = "3", features = ["rustls-0_23-webpki-roots"] } +clap = { version = "4.5.17", features = ["derive"] } +tokio-util = "0.7.12" +openidconnect = { version = "4.0.0", default-features = false, features = ["accept-rfc3339-timestamps"] } +encoding_rs = "0.8.35" +odbc-sys = { version = "0.28.0", optional = true } + + +[features] +default = [] +odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"] +lambda-web = ["dep:lambda-web", "odbc-static"] + +[dev-dependencies] +actix-http = "3" [build-dependencies] -awc = { version = "3", features = ["rustls-0_22-webpki-roots"] } +awc = { version = "3", features = ["rustls-0_23-webpki-roots"] } +rustls = "0.23" actix-rt = "2.8" libflate = "2" -futures-util = "0.3.21" +futures-util = "0.3.21" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aef94289..fb79c864 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,45 +1,19 @@ -FROM --platform=$BUILDPLATFORM rust:1.80-slim AS builder +FROM --platform=$BUILDPLATFORM rust:1.91-slim AS builder + WORKDIR /usr/src/sqlpage ARG TARGETARCH ARG BUILDARCH -RUN apt-get update && \ - if [ "$TARGETARCH" = "$BUILDARCH" ]; then \ - rustup target list --installed > TARGET && \ - echo gcc > LINKER && \ - apt-get install -y gcc libgcc-s1 && \ - cp /lib/*/libgcc_s.so.1 .; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - echo aarch64-unknown-linux-gnu > TARGET && \ - echo aarch64-linux-gnu-gcc > LINKER && \ - apt-get install -y gcc-aarch64-linux-gnu libgcc-s1-arm64-cross && \ - cp /usr/aarch64-linux-gnu/lib/libgcc_s.so.1 .; \ - elif [ "$TARGETARCH" = "arm" ]; then \ - echo armv7-unknown-linux-gnueabihf > TARGET && \ - echo arm-linux-gnueabihf-gcc > LINKER && \ - apt-get install -y gcc-arm-linux-gnueabihf libgcc-s1-armhf-cross && \ - cp /usr/arm-linux-gnueabihf/lib/libgcc_s.so.1 .; \ - else \ - echo "Unsupported cross compilation target: $TARGETARCH"; \ - exit 1; \ - fi && \ - rustup target add $(cat TARGET) && \ - cargo init . -# Build dependencies (creates a layer that avoids recompiling dependencies on every build) +COPY scripts/ /usr/local/bin/ +RUN cargo init . + +RUN /usr/local/bin/setup-cross-compilation.sh "$TARGETARCH" "$BUILDARCH" + COPY Cargo.toml Cargo.lock ./ -RUN cargo build \ - --target $(cat TARGET) \ - --config target.$(cat TARGET).linker='"'$(cat LINKER)'"' \ - --profile superoptimized +RUN /usr/local/bin/build-dependencies.sh -# Build the project COPY . . -RUN touch src/main.rs && \ - cargo build \ - --target $(cat TARGET) \ - --config target.$(cat TARGET).linker='"'$(cat LINKER)'"' \ - --profile superoptimized && \ - mv target/$(cat TARGET)/superoptimized/sqlpage sqlpage.bin +RUN /usr/local/bin/build-project.sh FROM busybox:glibc RUN addgroup --gid 1000 --system sqlpage && \ @@ -51,7 +25,8 @@ ENV SQLPAGE_WEB_ROOT=/var/www ENV SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage WORKDIR /var/www COPY --from=builder /usr/src/sqlpage/sqlpage.bin /usr/local/bin/sqlpage -COPY --from=builder /usr/src/sqlpage/libgcc_s.so.1 /lib/libgcc_s.so.1 +# Provide runtime helper libs in system lib directory for the glibc busybox base +COPY --from=builder /tmp/sqlpage-libs/* /lib/ USER sqlpage COPY --from=builder --chown=sqlpage:sqlpage /usr/src/sqlpage/sqlpage/sqlpage.db sqlpage/sqlpage.db EXPOSE 8080 diff --git a/README.md b/README.md index c5843625..66fcf064 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@ SQLpage [![A short video explaining the concept of sqlpage](./docs/sqlpage.gif)](./docs/sqlpage.mp4) -[SQLpage](https://sql.ophir.dev) is an **SQL**-only webapp builder. -It is meant for data scientists, analysts, and business intelligence teams -to build powerful data-centric applications quickly, -without worrying about any of the traditional web programming languages and concepts. +[SQLPage](https://sql-page.com) is an **SQL**-only webapp builder. +It allows building powerful data-centric user interfaces quickly, +by tranforming simple database queries into interactive websites. With SQLPage, you write simple `.sql` files containing queries to your database to select, group, update, insert, and delete your data, and you get good-looking clean webpages @@ -129,15 +128,23 @@ select - [PostgreSQL](https://www.postgresql.org/), and other compatible databases such as *YugabyteDB*, *CockroachDB* and *Aurora*. - [MySQL](https://www.mysql.com/), and other compatible databases such as *MariaDB* and *TiDB*. - [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server), and all compatible databases and providers such as *Azure SQL* and *Amazon RDS*. +- Any **ODBC-compatible database**, such as +[*ClickHouse*](https://github.com/ClickHouse/clickhouse-odbc), +[*MongoDB*](https://www.mongodb.com/docs/atlas/data-federation/query/sql/drivers/odbc/connect), +[*DuckDB*](https://duckdb.org/docs/stable/clients/odbc/overview.html), +[*Oracle*](https://www.oracle.com/database/technologies/releasenote-odbc-ic.html), +[*Snowflake*](https://docs.snowflake.com/en/developer-guide/odbc/odbc), +[*BigQuery*](https://cloud.google.com/bigquery/docs/reference/odbc-jdbc-drivers), +[*IBM DB2*](https://www.ibm.com/support/pages/db2-odbc-cli-driver-download-and-installation-information), and many others through their respective ODBC drivers. ## Get started -[Read the official *get started* guide on SQLPage's website](https://sql.ophir.dev/get_started.sql). +[Read the official *get started* guide on SQLPage's website](https://sql-page.com/get_started.sql). ### Using executables The easiest way to get started is to download the latest release from the -[releases page](https://github.com/lovasoa/SQLpage/releases). +[releases page](https://github.com/sqlpage/SQLPage/releases). - Download the binary that corresponds to your operating system (linux, macos, or windows). - Uncompress it: `tar -xzf sqlpage-*.tgz` @@ -145,7 +152,7 @@ The easiest way to get started is to download the latest release from the ### With docker -To run on a server, you can use [the docker image](https://hub.docker.com/r/lovasoa/sqlpage): +To run on a server, you can use [the docker image](https://hub.docker.com/r/lovasoa/SQLPage): - [Install docker](https://docs.docker.com/get-docker/) - In a terminal, run the following command: @@ -157,7 +164,7 @@ To run on a server, you can use [the docker image](https://hub.docker.com/r/lova custom components, and migrations (see [configuration.md](./configuration.md)) to `/etc/sqlpage` in the container. - For instance, you can use: - - `docker run -it --name sqlpage -p 8080:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm lovasoa/sqlpage` + - `docker run -it --name sqlpage -p 80:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm lovasoa/sqlpage` - And place your website in a folder named `source` and your `sqlpage.json` in a folder named `configuration`. - If you want to build your own docker image, taking the raw sqlpage image as a base is not recommended, since it is extremely stripped down and probably won't contain the dependencies you need. Instead, you can take debian as a base and simply copy the sqlpage binary from the official image to your own image: - ```Dockerfile @@ -176,6 +183,29 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f - In a terminal, run the following commands: - `brew install sqlpage` + +### ODBC Setup + +SQLPage supports ODBC connections to connect to databases that don't have native drivers. +You can skip this section if you want to use one of the built-in database drivers (SQLite, PostgreSQL, MySQL, Microsoft SQL Server). + +Linux and MacOS release binaries conatain a built-in statically linked ODBC driver manager (unixODBC). +You still need to install or provide the database-specific ODBC driver for the database you want to connect to. + +#### Install your ODBC database driver + - [DuckDB](https://duckdb.org/docs/stable/clients/odbc/overview.html) + - [Snowflake](https://docs.snowflake.com/en/developer-guide/odbc/odbc) + - [BigQuery](https://cloud.google.com/bigquery/docs/reference/odbc-jdbc-drivers) + - For other databases, follow your database's official odbc install instructions. + +#### Connect to your database + + - Find your [connection string](https://www.connectionstrings.com/). + - It will look like this: `Driver=/opt/snowflake_odbc/lib/libSnowflake.so;Server=xyz.snowflakecomputing.com;Database=MY_DB;Schema=PUBLIC;UID=my_user;PWD=my_password` + - It must reference the path to the database driver you installed earlier, plus any connection parameter required by the driver itself. Follow the instructions from the driver's own documentation. + - Use it in the [DATABASE_URL configuration option](./configuration.md) + + ## How it works ![architecture diagram](./docs/architecture-detailed.png) @@ -198,7 +228,7 @@ to the user's browser. - [Tiny splitwise clone](./examples/splitwise): a shared expense tracker app - [Corporate Conundrum](./examples/corporate-conundrum/): a board game implemented in SQL - [Master-Detail Forms](./examples/master-detail-forms/): shows how to implement a simple set of forms to insert data into database tables that have a one-to-many relationship. -- [SQLPage's own official website and documentation](./examples/official-site/): The SQL source code for the project's official site, https://sql.ophir.dev +- [SQLPage's own official website and documentation](./examples/official-site/): The SQL source code for the project's official site, https://sql-page.com - [Image gallery](./examples/image%20gallery%20with%20user%20uploads/): An image gallery where users can log in and upload images. Illustrates the implementation of a user authentication system using session cookies, and the handling of file uploads. - [User Management](./examples/user-authentication/): An authentication demo with user registration, log in, log out, and confidential pages. Uses PostgreSQL. - [Making a JSON API and integrating React components in the frontend](./examples/using%20react%20and%20other%20custom%20scripts%20and%20styles/): Shows how to integrate a react component in a SQLPage website, and how to easily build a REST API with SQLPage. @@ -269,59 +299,64 @@ SQLPage will re-parse a file from the database only when it has been modified. - [tabler icons](https://tabler-icons.io) is a large set of icons you can select directly from your SQL, - [handlebars](https://handlebarsjs.com/guide/) render HTML pages from readable templates for each component. -## Frequently asked questions +## Frequently Asked Questions -> Why would I want to write SQL instead of a real programming language? SQL is not even [Turing-complete](https://en.wikipedia.org/wiki/Turing_completeness)! +> **Why use SQL instead of a real programming language? SQL isn't even [Turing-complete](https://en.wikipedia.org/wiki/Turing_completeness)!** - - You are probably worrying about the wrong thing. If you can express your application in a purely declarative manner, you should propably do it, - even if you are using a traditional programming language. - It will be much more concise, readable, easy to reason about and to debug than any imperative code you could write. - - SQL is much more simple than traditional programming languages. It is often readable even by non-programmers, and yet it is very powerful. - - If you really want to make your website more complicated than it needs to be, please note that [SQL is actually Turing-complete](https://stackoverflow.com/questions/900055/is-sql-or-even-tsql-turing-complete/7580013#7580013). - - Even if it wasn't (if it didn't have recursive queries), a sequence of SQL statement executions driven by an user, like SQLPage allows you to do, would still be Turing-complete. You could build a sql website representing a Turing machine where the user would have to click "next" repeatedly to compute the next state of the machine. +- You're focusing on the wrong issue. If you can express your application declaratively, you should—whether using SQL or another language. Declarative code is often more concise, readable, easier to reason about, and easier to debug than imperative code. +- SQL is simpler than traditional languages, often readable by non-programmers, yet very powerful. +- If complexity is your goal, note that [SQL is actually Turing-complete](https://stackoverflow.com/questions/900055/is-sql-or-even-tsql-turing-complete/7580013#7580013). +- Even without recursive queries, a sequence of SQL statements driven by user interactions (like SQLPage) would still be Turing-complete, enabling you to build a SQL-powered website that functions as a Turing machine. -> Just Because You Can Doesn’t Mean You Should... -> -> — [someone being mean on reddit](https://www.reddit.com/r/rust/comments/14qjskz/comment/jr506nx) +> **Just Because You Can Doesn’t Mean You Should...** +— [someone being mean on Reddit](https://www.reddit.com/r/rust/comments/14qjskz/comment/jr506nx) -Life's too short to always play it safe. Where's the fun in *should*? -I think SQLPage has some real value to offer, despite its unconventional approach ! -SQLPage isn't intended to replace traditional web development frameworks or discourage their usage. -Instead, it caters to a specific group of people who want to leverage their SQL skills to rapidly build web applications, without spending months learning all of the technologies involved in traditional web development, -and then weeks using them to build a simple CRUD application. +It's not about "should" — it's about "why not?" +Keep coloring inside the lines if you want, but we'll be over here having fun with our SQL websites. -> Is this the same as Microsoft Access? +> **Is this the same as Microsoft Access?** -The goal is the same: make it easy to create simple data-centric applications. -But the tools are very different: - - SQLPage is a web server, not a desktop application. - - SQLPage is not a database, it _connects_ to real battle-tested databases. Microsoft Access tries to be a database, and it's [not very good at it](https://www.quora.com/Is-the-Access-database-reliable-and-secure). - - Microsoft Access is an expensive proprietary software, SQLPage is [open-source](./LICENSE.txt). - - Microsoft Access [is a zombie that will stab you in the back](https://medium.com/young-coder/microsoft-access-the-zombie-database-software-that-wont-die-5b09e389c166), SQLPage won't. - - SQLPage will not tortue you with [Visual Basic for Applications](https://en.wikipedia.org/wiki/Visual_Basic_for_Applications). +The goals are similar — creating simple data-centric applications — but the tools differ significantly: +- SQLPage is a web server, not a desktop app. +- SQLPage connects to existing robust relational databases; Access tries to **be** a database. +- Access is expensive and proprietary; SQLPage is [open-source](./LICENSE.txt). +- SQLPage spares you from the torment of [Visual Basic for Applications](https://en.wikipedia.org/wiki/Visual_Basic_for_Applications). -> Is the name a reference to Microsoft Frontpage ? +> **Is the name a reference to Microsoft FrontPage?** -Frontpage was a visual static website building software popular in the late 90s. -I had never heard of it before someone asked me this question. +FrontPage was a visual static website builder popular in the late '90s. I hadn't heard of it until someone asked. -> I like CSS, I want to design websites, not write SQL. +> **I like CSS. I want to design websites, not write SQL.** -Are you human ? [Human beings hate CSS](https://uxdesign.cc/why-do-most-developers-hate-css-63c92bce36ed). +If you want to write your own HTML and CSS, +you can [create custom components](https://sql-page.com/custom_components.sql) +by adding a [`.handlebars`](https://handlebarsjs.com/guide/) file in `sqlpage/templates` and writing your HTML and CSS there. ([Example](./sqlpage/templates/alert.handlebars)). +You can also use the `html` component to write raw HTML, or the `shell` component to include custom scripts and styles. -The take of SQLPage is that you should not spend time designing the border radius of your buttons until you have a working prototype. -We provide a set of components that look decent out of the box, so that you can focus on your data model instead. - -However, if you really want to write your own HTML and CSS, you can do it by creating your own components. -Just create a [`.handlebars`](https://handlebarsjs.com/guide/) file in `sqlpage/templates` and write your HTML and CSS in it. ([example](./sqlpage/templates/alert.handlebars)) +But SQLPage believes you shouldn't worry about button border radii until you have a working prototype. +We provide good-looking components out of the box so you can focus on your data model, and iterate quickly. ## Download SQLPage is available for download on the from multiple sources: -[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/lovasoa/sqlpage/total?label=direct%20download)](https://github.com/lovasoa/SQLpage/releases/latest) -[![Docker Pulls](https://img.shields.io/docker/pulls/lovasoa/sqlpage?label=docker%3A%20lovasoa%2Fsqlpage)](https://hub.docker.com/r/lovasoa/sqlpage) +[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/sqlpage/SQLPage/total?label=direct%20download)](https://github.com/sqlpage/SQLPage/releases/latest) +[![Docker Pulls](https://img.shields.io/docker/pulls/sqlpage/SQLPage?label=docker%3A%20lovasoa%2Fsqlpage)](https://hub.docker.com/r/sqlpage/SQLPage) [![homebrew downloads](https://img.shields.io/homebrew/installs/dq/sqlpage?label=homebrew%20downloads&labelColor=%232e2a24&color=%23f9d094)](https://formulae.brew.sh/formula/sqlpage#default) [![Scoop Version](https://img.shields.io/scoop/v/sqlpage?labelColor=%23696573&color=%23d7d4db)](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116) [![Crates.io Total Downloads](https://img.shields.io/crates/d/sqlpage?label=crates.io%20download&labelColor=%23264323&color=%23f9f7ec)](https://crates.io/crates/sqlpage) -[![](https://img.shields.io/badge/Nix-pkg-rgb(126,%20185,%20227))](https://search.nixos.org/packages?channel=unstable&show=sqlpage&from=0&size=50&sort=relevance&type=packages&query=sqlpage) \ No newline at end of file +[![](https://img.shields.io/badge/Nix-pkg-rgb(126,%20185,%20227))](https://search.nixos.org/packages?channel=unstable&show=sqlpage&from=0&size=50&sort=relevance&type=packages&query=sqlpage) + +## Contributing + +We welcome contributions! SQLPage is built with Rust and uses +vanilla javascript for its frontend parts. + +Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed instructions on development setup, testing, and pull request process. + +# Code signing policy + +Our windows binaries are digitally signed, so they should be recognized as safe by Windows. +Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/). [Contributors](https://github.com/sqlpage/SQLPage/graphs/contributors), [Owners](https://github.com/orgs/sqlpage/people?query=role%3Aowner). + +This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..c2cdeb58 --- /dev/null +++ b/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "includes": [ + "**", + "!examples/official-site/pgconf/**", + "!tests/end-to-end/test-results/**" + ], + "ignoreUnknown": true + }, + "formatter": { + "indentStyle": "space", + "indentWidth": 2 + }, + "vcs": { + "enabled": true, + "useIgnoreFile": true, + "clientKind": "git" + } +} diff --git a/build.rs b/build.rs index 3c2bdeea..3e594482 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ use actix_rt::spawn; +use actix_rt::time::sleep; use libflate::gzip; use std::collections::hash_map::DefaultHasher; use std::fs::File; @@ -11,19 +12,27 @@ use std::time::Duration; #[actix_rt::main] async fn main() { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); + println!("cargo:rerun-if-changed=build.rs"); let c = Rc::new(make_client()); for h in [ spawn(download_deps(c.clone(), "sqlpage.js")), spawn(download_deps(c.clone(), "sqlpage.css")), - spawn(download_deps(c.clone(), "tabler-icons.svg")), + spawn(download_tabler_icons( + c.clone(), + "https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.35.0/dist/tabler-sprite.svg", + )), spawn(download_deps(c.clone(), "apexcharts.js")), spawn(download_deps(c.clone(), "tomselect.js")), spawn(download_deps(c.clone(), "favicon.svg")), ] { h.await.unwrap(); } + set_odbc_rpath(); } fn make_client() -> awc::Client { @@ -35,13 +44,13 @@ fn make_client() -> awc::Client { /// Creates a file with inlined remote files included async fn download_deps(client: Rc, filename: &str) { - let path_in = format!("sqlpage/{}", filename); + let path_in = format!("sqlpage/{filename}"); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); let path_out: PathBuf = out_dir.join(filename); // Generate outfile by reading infile and interpreting all comments // like "/* !include https://... */" as a request to include the contents of // the URL in the generated file. - println!("cargo:rerun-if-changed={}", path_in); + println!("cargo:rerun-if-changed={path_in}"); let original = File::open(path_in).unwrap(); process_input_file(&client, &path_out, original).await; std::fs::write( @@ -67,7 +76,7 @@ async fn process_input_file(client: &awc::Client, path_out: &Path, original: Fil } outfile.write_all(b"\n").unwrap(); } else { - writeln!(outfile, "{}", line).unwrap(); + writeln!(outfile, "{line}").unwrap(); } } outfile @@ -99,21 +108,36 @@ fn copy_cached_to_opened_file(source: &Path, outfile: &mut impl std::io::Write) } async fn download_url_to_path(client: &awc::Client, url: &str, path: &Path) { - let mut resp = client.get(url).send().await.unwrap_or_else(|err| { - let path = make_url_path(url); - panic!( - "We need to download external frontend dependencies to build the static frontend. \n\ - Could not download static asset. You can manually download the file with: \n\ - curl {url:?} > {path:?} \n\ - {err}" - ) - }); - if resp.status() != 200 { - panic!("Received {} status code from {}", resp.status(), url); + let mut attempt = 1; + let max_attempts = 2; + + loop { + match client.get(url).send().await { + Ok(mut resp) => { + if resp.status() != 200 { + panic!("Received {} status code from {}", resp.status(), url); + } + let bytes = resp.body().limit(128 * 1024 * 1024).await.unwrap(); + std::fs::write(path, &bytes) + .expect("Failed to write external frontend dependency to local file"); + break; + } + Err(err) => { + if attempt >= max_attempts { + let path = make_url_path(url); + panic!( + "We need to download external frontend dependencies to build the static frontend. \n\ + Could not download static asset after {max_attempts} attempts. You can manually download the file with: \n\ + curl {url:?} > {path:?} \n\ + {err}" + ); + } + sleep(Duration::from_secs(1)).await; + println!("cargo:warning=Retrying download of {url} after {err}."); + attempt += 1; + } + } } - let bytes = resp.body().limit(128 * 1024 * 1024).await.unwrap(); - std::fs::write(path, &bytes) - .expect("Failed to write external frontend dependency to local file"); } // Given a filename, creates a new unique filename based on the file contents @@ -151,3 +175,43 @@ fn make_url_path(url: &str) -> PathBuf { ); sqlpage_artefacts.join(filename) } + +async fn download_tabler_icons(client: Rc, sprite_url: &str) { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let icon_map_path = out_dir.join("icons.rs"); + let mut sprite_content = Vec::with_capacity(3 * 1024 * 1024); + copy_url_to_opened_file(&client, sprite_url, &mut sprite_content).await; + let mut file = File::create(icon_map_path).unwrap(); + file.write_all(b"[").unwrap(); + extract_icons_from_sprite(&sprite_content, |name, content| { + writeln!(file, "({name:?}, r#\"{content}\"#),").unwrap(); + }); + file.write_all(b"]").unwrap(); +} + +fn extract_icons_from_sprite(sprite_content: &[u8], mut callback: impl FnMut(&str, &str)) { + let mut sprite_str = std::str::from_utf8(sprite_content).unwrap(); + fn take_between<'a>(s: &mut &'a str, start: &str, end: &str) -> Option<&'a str> { + let start_index = s.find(start)?; + let end_index = s[start_index + start.len()..].find(end)?; + let result = &s[start_index + start.len()..][..end_index]; + *s = &s[start_index + start.len() + end_index + end.len()..]; + Some(result) + } + while let Some(mut symbol_tag) = take_between(&mut sprite_str, "") { + let id = take_between(&mut symbol_tag, "id=\"tabler-", "\"").expect("id not found"); + let content_start = symbol_tag.find('>').unwrap() + 1; + callback(id, &symbol_tag[content_start..]); + } +} + +/// On debian-based linux distributions, odbc drivers are installed in /usr/lib/-linux-gnu/odbc +/// which is not in the default library search path. +fn set_odbc_rpath() { + if cfg!(all(target_os = "linux", feature = "odbc-static")) { + println!( + "cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/{}-linux-gnu/odbc", + std::env::var("TARGET").unwrap().split('-').next().unwrap() + ); + } +} diff --git a/configuration.md b/configuration.md index 359a98c9..faa15d79 100644 --- a/configuration.md +++ b/configuration.md @@ -9,9 +9,11 @@ Here are the available configuration options and their default values: | variable | default | description | | --------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `listen_on` | 0.0.0.0:8080 | Interface and port on which the web server should listen | -| `database_url` | sqlite://sqlpage.db?mode=rwc | Database connection URL, in the form `dbname://user:password@host:port/dbname`. Special characters in user and password should be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). | +| `database_url` | sqlite://sqlpage.db?mode=rwc | Database connection URL, in the form `dbengine://user:password@host:port/dbname`. Special characters in user and password should be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). | +| `database_password` | | Database password. If set, this will override any password specified in the `database_url`. This allows you to keep the password separate from the connection string for better security. | | `port` | 8080 | Like listen_on, but specifies only the port. | | `unix_socket` | | Path to a UNIX socket to listen on instead of the TCP port. If specified, SQLPage will accept HTTP connections only on this socket and not on any TCP port. This option is mutually exclusive with `listen_on` and `port`. +| `host` | | The web address where your application is accessible (e.g., "myapp.example.com"). Used for login redirects with OIDC. | | `max_database_pool_connections` | PostgreSQL: 50
MySql: 75
SQLite: 16
MSSQL: 100 | How many simultaneous database connections to open at most | | `database_connection_idle_timeout_seconds` | SQLite: None
All other: 30 minutes | Automatically close database connections after this period of inactivity | | `database_connection_max_lifetime_seconds` | SQLite: None
All other: 60 minutes | Always close database connections after this amount of time | @@ -20,18 +22,28 @@ Here are the available configuration options and their default values: | `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` | | `web_root` | `.` | The root directory of the web server, where the `index.sql` file is located. | | `site_prefix` | `/` | Base path of the site. If you want to host SQLPage at `https://example.com/sqlpage/`, set this to `/sqlpage/`. When using a reverse proxy, this allows hosting SQLPage together with other applications on the same subdomain. | -| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to [`templates/`](https://sql.ophir.dev/custom_components.sql), [`migrations/`](https://sql.ophir.dev/your-first-sql-website/migrations.sql), and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. Be careful not to use a path that is accessible from the public WEB_ROOT | +| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to [`templates/`](https://sql-page.com/custom_components.sql), [`migrations/`](https://sql-page.com/your-first-sql-website/migrations.sql), and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. Be careful not to use a path that is accessible from the public WEB_ROOT | | `allow_exec` | false | Allow usage of the `sqlpage.exec` function. Do this only if all users with write access to sqlpage query files and to the optional `sqlpage_files` table on the database are trusted. | -| `max_uploaded_file_size` | 5242880 | Maximum size of uploaded files in bytes. Defaults to 5 MiB. | +| `max_uploaded_file_size` | 5242880 | Maximum size of forms and uploaded files in bytes. Defaults to 5 MiB. | +| `oidc_protected_paths` | `["/"]` | A list of URL prefixes that should be protected by OIDC authentication. By default, all paths are protected (`["/"]`). If you want to make some pages public, you can restrict authentication to a sub-path, for instance `["/admin", "/users/settings"]`. | +| `oidc_public_paths` | `[]` | A list of URL prefixes that should be publicly available. By default, no paths are publicly accessible (`[]`). If you want to make some pages public, you can bypass authentication for a sub-path, for instance `["/public/", "/assets/"]`. Keep in mind that without the closing backslashes, that any directory or file starting with `public` or `assets` will be publicly available. This will also overwrite any protected path restriction. If you have a private path `/private` and you define the public path `/private/public/` everything in `/private/public/` will be publicly accessible, while everything else in private will still need authentication. +| `oidc_issuer_url` | | The base URL of the [OpenID Connect provider](#openid-connect-oidc-authentication). Required for enabling Single Sign-On. | +| `oidc_client_id` | sqlpage | The ID that identifies your SQLPage application to the OIDC provider. You get this when registering your app with the provider. | +| `oidc_client_secret` | | The secret key for your SQLPage application. Keep this confidential as it allows your app to authenticate with the OIDC provider. | +| `oidc_scopes` | openid email profile | Space-separated list of [scopes](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) your app requests from the OIDC provider. | +| `oidc_additional_trusted_audiences` | unset | A list of additional audiences that are allowed in JWT tokens, beyond the client ID. When empty or unset, any additional audience is accepted. For increased security, set to an empty list `[]` to only allow the client ID as audience. | | `max_pending_rows` | 256 | Maximum number of rendered rows that can be queued up in memory when a client is slow to receive them. | -| `compress_responses` | true | When the client supports it, compress the http response body. This can save bandwidth and speed up page loading on slow connections, but can also increase CPU usage and cause rendering delays on pages that take time to render (because streaming responses are buffered for longer than necessary). | +| `compress_responses` | false | When the client supports it, compress the http response body. This can save bandwidth and speed up page loading on slow connections, but can also increase CPU usage and cause rendering delays on pages that take time to render (because streaming responses are buffered for longer than necessary). | | `https_domain` | | Domain name to request a certificate for. Setting this parameter will automatically make SQLPage listen on port 443 and request an SSL certificate. The server will take a little bit longer to start the first time it has to request a certificate. | | `https_certificate_email` | contact@ | The email address to use when requesting a certificate. | | `https_certificate_cache_dir` | ./sqlpage/https | A writeable directory where to cache the certificates, so that SQLPage can serve https traffic immediately when it restarts. | | `https_acme_directory_url` | https://acme-v02.api.letsencrypt.org/directory | The URL of the ACME directory to use when requesting a certificate. | | `environment` | development | The environment in which SQLPage is running. Can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk. | -| `content_security_policy` | `script-src 'self' 'nonce-XXX` | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. | +| `content_security_policy` | `script-src 'self' 'nonce-{NONCE}'` | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. If you want a custom CSP that contains a nonce, include the `'nonce-{NONCE}'` directive in your configuration string and it will be populated with a random value per request. | | `system_root_ca_certificates` | false | Whether to use the system root CA certificates to validate SSL certificates when making http requests with `sqlpage.fetch`. If set to false, SQLPage will use its own set of root CA certificates. If the `SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables are set, they will be used instead of the system root CA certificates. | +| `max_recursion_depth` | 10 | Maximum depth of recursion allowed in the `run_sql` function. Maximum value is 255. | +| `markdown_allow_dangerous_html` | false | Whether to allow raw HTML in markdown content. Only enable this if the markdown content is fully trusted (not user generated). | +| `markdown_allow_dangerous_protocol` | false | Whether to allow dangerous protocols (like javascript:) in markdown links. Only enable this if the markdown content is fully trusted (not user generated). | Multiple configuration file formats are supported: you can use a [`.json5`](https://json5.org/) file, a [`.toml`](https://toml.io/) file, or a [`.yaml`](https://en.wikipedia.org/wiki/YAML#Syntax) file. @@ -54,7 +66,7 @@ If you have a `.env` file in the current directory or in any of its parent direc The `database_url` parameter sets all the connection parameters for the database, including - - the database type (`sqlite`, `postgres`, `mysql`, `mssql`, etc.) + - the database engine type (`sqlite`, `postgres`, `mysql`, `mssql`, or ODBC connection strings) - the username and password - the host (or ip adress) and port - the database name @@ -67,7 +79,7 @@ The `database_url` parameter sets all the connection parameters for the database - `application_name=my_application` for PostgreSQL to set the application name, which can be useful for monitoring and logging on the database server side. - `collation=utf8mb4_unicode_ci` for MySQL to set the collation of the connection -All the parameters need to be properly [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) if they contain special characters. +All the parameters need to be properly [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) if they contain special characters like `@` (`%40`), `:` (`%3A`), `/` (`%2F`), `?` (`%3F`), `#` (`%23`). A full connection string for a PostgreSQL database might look like this: @@ -75,10 +87,89 @@ A full connection string for a PostgreSQL database might look like this: postgres://my_user:p%40ss@localhost:5432/my_database?sslmode=verify-ca&sslrootcert=/path/to/ca.pem&sslcert=/path/to/cert.pem&sslkey=/path/to/key.pem&application_name=my_application ``` +#### ODBC Connection Strings + +For ODBC-compatible databases (Oracle, Snowflake, BigQuery, IBM DB2, etc.), you can use ODBC connection strings directly: + +```bash +# Using a Data Source Name (DSN) +DATABASE_URL="DSN=MyDatabase" + +# Using inline connection parameters +DATABASE_URL="Driver={PostgreSQL};Server=localhost;Port=5432;Database=mydb;UID=myuser;PWD=mypassword" + +# Oracle example +DATABASE_URL="Driver={Oracle ODBC Driver};Server=localhost:1521/XE;UID=hr;PWD=password" + +# Snowflake example +DATABASE_URL="Driver={SnowflakeDSIIDriver};Server=account.snowflakecomputing.com;Database=mydb;UID=user;PWD=password" +``` + +ODBC drivers must be installed and configured on your system. On Linux, the `unixODBC` driver manager is statically linked into the SQLPage binary, so you usually only need to install and configure the database-specific ODBC driver for your target database (for example Snowflake, Oracle, DuckDB...). + +If the `database_password` configuration parameter is set, it will override any password specified in the `database_url`. +It does not need to be percent-encoded. +This allows you to keep the password separate from the connection string, which can be useful for security purposes, especially when storing configurations in version control systems. + +### OpenID Connect (OIDC) Authentication + +OpenID Connect (OIDC) is a secure way to let users log in to your SQLPage application using their existing accounts from popular services. When OIDC is configured, you can control which parts of your application require authentication using the `oidc_protected_paths` option. By default, all pages are protected. You can specify a list of URL prefixes to protect specific areas, allowing you to have a mix of public and private pages. + +To set up OIDC, you'll need to: +1. Register your application with an OIDC provider +2. Configure the provider's details in SQLPage + +#### Setting Your Application's Address + +When users log in through an OIDC provider, they need to be sent back to your application afterward. For this to work correctly, you need to tell SQLPage where your application is located online: + +- Use the `host` setting to specify your application's web address (for example, "myapp.example.com") +- If you already have the `https_domain` setting set (to fetch https certificates for your site), then you don't need to duplicate it into `host`. + +Example configuration: +```json +{ + "oidc_issuer_url": "https://accounts.google.com", + "oidc_client_id": "your-client-id", + "oidc_client_secret": "your-client-secret", + "host": "myapp.example.com" +} +``` + +#### Cloud Identity Providers + +- **Google** + - Documentation: https://developers.google.com/identity/openid-connect/openid-connect + - Set *oidc_issuer_url* to `https://accounts.google.com` + +- **Microsoft Entra ID** (formerly Azure AD) + - Documentation: https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app + - Set *oidc_issuer_url* to `https://login.microsoftonline.com/{tenant}/v2.0` + - ([Find your tenant name](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri)) + +- **GitHub** + - Issuer URL: `https://github.com` + - Documentation: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps + +#### Self-Hosted Solutions + +- **Keycloak** + - Issuer URL: `https://your-keycloak-server/auth/realms/your-realm` + - [Setup Guide](https://www.keycloak.org/getting-started/getting-started-docker) + +- **Authentik** + - Issuer URL: `https://your-authentik-server/application/o/your-application` + - [Setup Guide](https://goauthentik.io/docs/providers/oauth2) + +After registering your application with the provider, you'll receive a client ID and client secret. These are used to configure SQLPage to work with your chosen provider. + +Note: OIDC is optional. If you don't configure it, your SQLPage application will be accessible without authentication. + ### Example `.env` file ```bash -DATABASE_URL="sqlite:///path/to/my_database.db?mode=rwc" +DATABASE_URL="postgres://my_user@localhost:5432/my_database?sslmode=verify-ca&sslrootcert=/path/to/ca.pem" +DATABASE_PASSWORD="my_secure_password" SQLITE_EXTENSIONS="mod_spatialite crypto define regexp" ``` @@ -98,9 +189,35 @@ For instance, if you want to create a custom `my_component` component, that disp ``` -[See the full custom component documentation](https://sql.ophir.dev/custom_components.sql). +[See the full custom component documentation](https://sql-page.com/custom_components.sql). + +## Directories + +SQLPage needs two important directories to work: the configuration directory, and the web root. + +### Configuration directory + +The configuration directory is the `./sqlpage/` folder by default. +In the [official docker image](https://hub.docker.com/r/lovasoa/sqlpage), it is located in `/etc/sqlpage/`. +It can be configured using the `--config-dir` command-line argument, or the `SQLPAGE_CONFIGURATION_DIRECTORY` environment variable. -## Connection initialization scripts +It can contain + + - the [`sqlpage.json`](#configuring-sqlpage) configuration file, + - the [`templates`](#custom-components) directory, + - the [`migrations`](#migrations) directory, + - the [connection management](#connection-management) sql files. + +### Web Root + +The web root is where you place your sql files. +By default, it is set to the current working directory, from which the sqlpage binary is launched. +In the [official docker image](https://hub.docker.com/r/lovasoa/sqlpage), the web root is set to `/var/www`. +It can be configured using the `--web-root` command-line argument, or the `SQLPAGE_WEB_ROOT` environment variable. + +## Connection management + +### Connection initialization scripts SQLPage allows you to run a SQL script when a new database connection is opened, by simply creating a `sqlpage/on_connect.sql` file. @@ -128,3 +245,60 @@ CREATE TEMPORARY TABLE my_temporary_table( my_temp_column TEXT ); ``` + +### Connection cleanup scripts: `on_reset.sql` + +SQLPage allows you to run a SQL script after a request has been processed, +by simply creating a `sqlpage/on_reset.sql` file. + +This can be useful to clean up temporary tables, +rollback transactions that were left open, +or other resources that were created during the request. + +You can also use this script to close database connections that are +in an undesirable state, such as being in a transaction that was left open. +To close a connection, write a select statement that returns a single row +with a single boolean column named `is_healthy`, and set it to false. + +#### Rollback transactions + +You can automatically rollback any open transactions +when a connection is returned to the pool, +so that a new request is never executed in the context of an open transaction from a previous request. + +For this to work, you need to create a `sqlpage/on_reset.sql` containing the following line: + +```sql +ROLLBACK; +``` + +#### Cleaning up all connection state + +Some databases allow you to clean up all the state associatPed with a connection. + +##### PostgreSQL + +By creating a `sqlpage/on_reset.sql` file containing a [`DISCARD ALL`](https://www.postgresql.org/docs/current/sql-discard.html) statement. + +```sql +DISCARD ALL; +``` + +##### SQL Server + +By creating a `sqlpage/on_reset.sql` file containing a call to the [`sp_reset_connection`](https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/system-stored-procedures-transact-sql?view=sql-server-ver16#api-system-stored-procedures) stored procedure. + +```sql +EXEC sp_reset_connection; +``` + +## Migrations + +SQLPage allows you to run SQL scripts when the database schema changes, by creating a `sqlpage/migrations` directory. +We have a guide on [how to create migrations](https://sql-page.com/your-first-sql-website/migrations.sql). + +## Custom URL routes + +By default, SQLPage encourages a simple mapping between the URL and the SQL file that is executed. +You can also create custom URL routes by creating [`404.sql` files](https://sql-page.com/your-first-sql-website/custom_urls.sql). +If you need advanced routing, you can also [add a reverse proxy in front of SQLPage](https://sql-page.com/your-first-sql-website/nginx.sql). diff --git a/db-test-setup/postgres/Dockerfile b/db-test-setup/postgres/Dockerfile index 6cdc77fa..6ae39292 100644 --- a/db-test-setup/postgres/Dockerfile +++ b/db-test-setup/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:16-alpine +FROM postgres:17-alpine # Copy the SSL certificates diff --git a/docker-compose.yml b/docker-compose.yml index 2f3086a9..b67355d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,15 @@ # You can easily switch between different databases by changing the value of COMPOSE_PROFILES in the .env file. + +# possible database connection strings: +# DATABASE_URL='postgres://root:Password123!@localhost/sqlpage' +# DATABASE_URL='mssql://root:Password123!@localhost/sqlpage' +# DATABASE_URL='mysql://root:Password123!@localhost/sqlpage' +# DATABASE_URL='Driver={/usr/lib64/psqlodbcw.so};Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!' + +# Run for instance: +# docker compose up postgres +# and in another terminal: +# DATABASE_URL='db_url' cargo test services: web: build: { context: "." } @@ -31,6 +42,7 @@ services: environment: MYSQL_ROOT_PASSWORD: Password123! MYSQL_DATABASE: sqlpage + mssql: profiles: ["mssql"] ports: ["1433:1433"] @@ -41,10 +53,11 @@ services: timeout: 3s retries: 10 start_period: 10s + mariadb: profiles: ["mariadb"] ports: ["3306:3306"] image: mariadb environment: MYSQL_ROOT_PASSWORD: Password123! - MYSQL_DATABASE: sqlpage \ No newline at end of file + MYSQL_DATABASE: sqlpage diff --git a/docs/introducing-sqlpage-to-the-postgres-community.md b/docs/introducing-sqlpage-to-the-postgres-community.md index 6586e0fe..b7f9d9da 100644 --- a/docs/introducing-sqlpage-to-the-postgres-community.md +++ b/docs/introducing-sqlpage-to-the-postgres-community.md @@ -2,7 +2,7 @@ SQLPage is an open-source tool that empowers database people to quickly build beautiful dynamic web applications *entirely in SQL*. -Designed to seamlessly integrate with PostgreSQL, SQLPage enables data practitioners to leverage their SQL skills to create robust, data-centric web apps without the need for traditional web programming languages, thanks to its [rich library of built-in web components](https://sql.ophir.dev/documentation.sql) that can be invoked directly from basic SQL queries. +Designed to seamlessly integrate with PostgreSQL, SQLPage enables data practitioners to leverage their SQL skills to create robust, data-centric web apps without the need for traditional web programming languages, thanks to its [rich library of built-in web components](https://sql-page.com/documentation.sql) that can be invoked directly from basic SQL queries. It lets you create complex dynamic webapps for data analysis, visualization, data ingestion, internal tooling, administration panels, prototyping, and more just by writing simple standard `.sql` files. @@ -29,7 +29,7 @@ SQLPage opens the world of easy web application development to database speciali ## Example -Here are the exact two SQL queries that builds the list of components of the documentation page on [SQLPage's official website](https://sql.ophir.dev) +Here are the exact two SQL queries that builds the list of components of the documentation page on [SQLPage's official website](https://sql-page.com) ``` SELECT 'list' AS component, 'components' AS title; @@ -48,10 +48,10 @@ order by name; ## Get Started -To explore the possibilities and limitations of SQLPage, visit [the official website](https://sql.ophir.dev) and read the [SQL website building tutorial](https://sql.ophir.dev/get%20started.sql). Join the [SQLPage community](https://github.com/lovasoa/SQLpage/discussions) to discuss your PostgreSQL-powered web applications. +To explore the possibilities and limitations of SQLPage, visit [the official website](https://sql-page.com) and read the [SQL website building tutorial](https://sql-page.com/get%20started.sql). Join the [SQLPage community](https://github.com/sqlpage/SQLPage/discussions) to discuss your PostgreSQL-powered web applications. ## Contributing -SQLPage is an open-source project, and contributions from the PostgreSQL community are highly encouraged. Visit [the GitHub repository](https://github.com/lovasoa/sqlpage) to contribute, report issues, or submit feature requests. +SQLPage is an open-source project, and contributions from the PostgreSQL community are highly encouraged. Visit [the GitHub repository](https://github.com/sqlpage/SQLPage) to contribute, report issues, or submit feature requests. Discover the power of SQL-driven web application development with SQLPage and take your PostgreSQL experience to new heights! \ No newline at end of file diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 00000000..8ce8cc99 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/logo.webp b/docs/logo.webp new file mode 100644 index 00000000..80cc5da6 Binary files /dev/null and b/docs/logo.webp differ diff --git a/docs/sqlpage for sqlite.md b/docs/sqlpage for sqlite.md index 6a681972..4328742d 100644 --- a/docs/sqlpage for sqlite.md +++ b/docs/sqlpage for sqlite.md @@ -6,11 +6,11 @@ I'm not sure whether announcements like this are allowed here; feel free to dele I wanted to introduce a cool piece of open source software I have been working on for a long time, and that is now ready for more general use. -It's called [SQLPage](https://sql.ophir.dev), and it lets you build a full web application on top of your SQLite database using nothing more than standard SQL queries. +It's called [SQLPage](https://sql-page.com), and it lets you build a full web application on top of your SQLite database using nothing more than standard SQL queries. -# SQLPage: [build a website in SQL](https://sql.ophir.dev) +# SQLPage: [build a website in SQL](https://sql-page.com) -[![code-screenshots](https://github.com/lovasoa/SQLpage/assets/552629/03ed65bc-ecb1-4c01-990e-d6ab97be39c0)](https://github.com/lovasoa/SQLPage) +[![code-screenshots](https://github.com/sqlpage/SQLPage/assets/552629/03ed65bc-ecb1-4c01-990e-d6ab97be39c0)](https://github.com/sqlpage/SQLPage) ## ❓ What is it ? @@ -40,6 +40,6 @@ Some cool things people are building with SQLPage: ## Open-Source - - [Official project page](https://sql.ophir.dev) - - [Source Code on Github](https://github.com/lovasoa/SQLPage) - - [Examples](https://github.com/lovasoa/SQLpage/tree/main/examples) \ No newline at end of file + - [Official project page](https://sql-page.com) + - [Source Code on Github](https://github.com/sqlpage/SQLPage) + - [Examples](https://github.com/sqlpage/SQLPage/tree/main/examples) \ No newline at end of file diff --git a/examples/CRUD - Authentication/README.md b/examples/CRUD - Authentication/README.md index ba752063..a5f7d02e 100644 --- a/examples/CRUD - Authentication/README.md +++ b/examples/CRUD - Authentication/README.md @@ -10,14 +10,14 @@ Three files (login.sql, logout.sql, and create_session.sql) implement authentica 2. Session checking code snippet at the top of the protected page checks if a valid session token (cookie) is set. In this example, the SET statement sets a local variable, `$_username`, for later use: ```sql -- Checks if a valid session token cookie is available -SET $_username = ( +set _username = ( SELECT username FROM sessions WHERE sqlpage.cookie('session_token') = id AND created_at > datetime('now', '-1 day') ); ``` -3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `SET $_session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql): +3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `set _session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql): ```sql SELECT 'redirect' AS component, @@ -34,8 +34,8 @@ WHERE $_username IS NULL AND $_session_required; Because the same code is used for session token check for all protected pages, it makes sense to place it in a separate module (header_shell_session.sql) and execute it via run_sql() at the top of protected files: ```sql -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -104,9 +104,9 @@ The `$_shell_enabled` variable controls the execution of the custom shell compon The header modules expects that the calling module sets several variables. The SET statement makes it possible to check if the variables are set appropriately in one place at the beginning of the module, rather then placing guards every time theses variables are used. Hence, the top section of the header file includes ```sql -SET $_curpath = ifnull($_curpath, '/'); -SET $_session_required = ifnull($_session_required, 1); -SET $_shell_enabled = ifnull($_shell_enabled, 1); +set _curpath = ifnull($_curpath, '/'); +set _session_required = ifnull($_session_required, 1); +set _shell_enabled = ifnull($_shell_enabled, 1); ``` In this case, if any required variable is not set, a suitable default value is defined, so that the following code would not have to check for NULL values. Alternatively, a redirect to an error page may be used, to inform the programmer about the potential issue. @@ -142,8 +142,8 @@ All three module load the footer module discussed above that produces a conditio All three modules provide access to the database and are treated as protected: they are only accessible to authenticated users. Hence, they start with (mostly) the same code block: ```sql -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -164,7 +164,7 @@ The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by The rest of the table view module is fairly basic. It defines two alerts for displaying confirmation and error messages, a "new record" button, and the table itself. The last "actions" column is added to the table, designated as markdown, and includes shortcuts to edit/delete the corresponding record. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/table_view.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/table_view.png) ### Detail view @@ -178,7 +178,7 @@ SELECT $_curpath AS link WHERE $id = '' OR CAST($id AS INT) = 0; -SET $error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); +set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); SELECT 'redirect' AS component, $_curpath || '?error=' || $error_msg AS link @@ -190,7 +190,7 @@ The blank string and zero are considered the equivalents of NULL, so redirect to Another accepted GET URL parameter is $values, which may be set to a JSON representation of the record. This parameter is returned from the currencies_item_dml.sql script if the database operation fails. Then the detail view will display an error message, but the form will remain populated with the user-submitted data. If $values is set, it takes precedence. This check throws an error if $values is set, but does not represent a valid JSON. ```sql -SET $_err_msg = +set _err_msg = sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __'; SELECT @@ -201,7 +201,7 @@ WHERE NOT json_valid($values); The detail view maybe called with zero, one, or two (\$id/\$values) parameters. Invalid values are filtered out at this point, so the next step is to check provided parameters and determine the dataset that should go into the form. ```sql -SET $_values = ( +set _values = ( WITH fields AS ( SELECT id, name, to_rub @@ -229,7 +229,7 @@ SET $_values = ( Each of the three united SELECTs in the "fields" CTE returns a single row and only one of them is selected for any given combination of \$id/\$values using the WHERE clauses. This query returns the "final" set of fields as a JSON object. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/detail_view.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/detail_view.png) Now that the input parameters are validated and the "final" dataset is determined, it is the time to define the form GUI elements. First, I define the button to switch to the table view. Note that the same form is used to confirm record deletion, and when this happens, the "Browse" button is not shown. @@ -253,7 +253,7 @@ WHERE NOT ifnull($action = 'DELETE', FALSE); The following section defines the main form with record fields. First the $\_valid_ids variable is constructed as the source for the drop-down id field. The code also adds the NULL value used for defining a new record. Note that, when this form is opened from the table view via the "New Record" button, the $action variable is set to "INSERT" and the id field is set to the empty array in the first assignment via the alternative UINION and to the single NULL in the second assignment. The two queries can also be combined relatively straightforwardly using CTEs. ```sql -SET $_valid_ids = ( +set _valid_ids = ( SELECT json_group_array( json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id ) @@ -263,7 +263,7 @@ SET $_valid_ids = ( SELECT '[]' WHERE $action = 'INSERT' ); -SET $_valid_ids = ( +set _valid_ids = ( json_insert($_valid_ids, '$[#]', json_object('label', 'NULL', 'value', json('null')) ) @@ -274,7 +274,7 @@ The next part defines form fields via the "dynamic" component (for some reason I Also note that this single form definition actually combines two forms (the second being the record delete confirmation form). If the $action variable is set to "DELETE" (after the delete operation is initiated from either the table or detail view), buttons are adjusted appropriately and all fields are set to read-only. Whether this is a good design is a separate question. Perhaps, defining two separate forms is a better approach. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/delete_confirmation.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/delete_confirmation.png) After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed. diff --git a/examples/CRUD - Authentication/sqlpage/templates/shell.handlebars b/examples/CRUD - Authentication/sqlpage/templates/shell.handlebars index ef752cb6..2771f849 100644 --- a/examples/CRUD - Authentication/sqlpage/templates/shell.handlebars +++ b/examples/CRUD - Authentication/sqlpage/templates/shell.handlebars @@ -195,7 +195,7 @@ {{{markdown footer}}} {{else}} - Built with SQLPage + Built with SQLPage {{/if}} diff --git a/examples/CRUD - Authentication/www/README.md b/examples/CRUD - Authentication/www/README.md index 01626831..81a59bb6 100644 --- a/examples/CRUD - Authentication/www/README.md +++ b/examples/CRUD - Authentication/www/README.md @@ -10,14 +10,14 @@ Three files (login.sql, logout.sql, and create_session.sql) implement authentica 2. Session checking code snippet at the top of the protected page checks if a valid session token (cookie) is set. In this example, the SET statement sets a local variable, `$_username`, for later use: ```sql -- Checks if a valid session token cookie is available -SET $_username = ( +set _username = ( SELECT username FROM sessions WHERE sqlpage.cookie('session_token') = id AND created_at > datetime('now', '-1 day') ); ``` -3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `SET $_session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql): +3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `set _session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql): ```sql SELECT 'redirect' AS component, @@ -34,8 +34,8 @@ WHERE $_username IS NULL AND $_session_required; Because the same code is used for session token check for all protected pages, it makes sense to place it in a separate module (header_shell_session.sql) and execute it via run_sql() at the top of protected files: ```sql -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -104,9 +104,9 @@ The `$_shell_enabled` variable controls the execution of the custom shell compon The header modules expects that the calling module sets several variables. The SET statement makes it possible to check if the variables are set appropriately in one place at the beginning of the module, rather then placing guards every time theses variables are used. Hence, the top section of the header file includes ```sql -SET $_curpath = ifnull($_curpath, '/'); -SET $_session_required = ifnull($_session_required, 1); -SET $_shell_enabled = ifnull($_shell_enabled, 1); +set _curpath = ifnull($_curpath, '/'); +set _session_required = ifnull($_session_required, 1); +set _shell_enabled = ifnull($_shell_enabled, 1); ``` In this case, if any required variable is not set, a suitable default value is defined, so that the following code would not have to check for NULL values. Alternatively, a redirect to an error page may be used, to inform the programmer about the potential issue. @@ -142,8 +142,8 @@ All three module load the footer module discussed above that produces a conditio All three modules provide access to the database and are treated as protected: they are only accessible to authenticated users. Hence, they start with (mostly) the same code block: ```sql -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -164,7 +164,7 @@ The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by The rest of the table view module is fairly basic. It defines two alerts for displaying confirmation and error messages, a "new record" button, and the table itself. The last "actions" column is added to the table, designated as markdown, and includes shortcuts to edit/delete the corresponding record. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/table_view.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/table_view.png) ### Detail view @@ -178,7 +178,7 @@ SELECT $_curpath AS link WHERE $id = '' OR CAST($id AS INT) = 0; -SET $error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); +set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); SELECT 'redirect' AS component, $_curpath || '?error=' || $error_msg AS link @@ -190,7 +190,7 @@ The blank string and zero are considered the equivalents of NULL, so redirect to Another accepted GET URL parameter is $values, which may be set to a JSON representation of the record. This parameter is returned from the currencies_item_dml.sql script if the database operation fails. Then the detail view will display an error message, but the form will remain populated with the user-submitted data. If $values is set, it takes precedence. This check throws an error if $values is set, but does not represent a valid JSON. ```sql -SET $_err_msg = +set _err_msg = sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __'; SELECT @@ -201,7 +201,7 @@ WHERE NOT json_valid($values); The detail view maybe called with zero, one, or two (\$id/\$values) parameters. Invalid values are filtered out at this point, so the next step is to check provided parameters and determine the dataset that should go into the form. ```sql -SET $_values = ( +set _values = ( WITH fields AS ( SELECT id, name, to_rub @@ -229,7 +229,7 @@ SET $_values = ( Each of the three united SELECTs in the "fields" CTE returns a single row and only one of them is selected for any given combination of \$id/\$values using the WHERE clauses. This query returns the "final" set of fields as a JSON object. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/detail_view.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/detail_view.png) Now that the input parameters are validated and the "final" dataset is determined, it is the time to define the form GUI elements. First, I define the button to switch to the table view. Note that the same form is used to confirm record deletion, and when this happens, the "Browse" button is not shown. @@ -253,7 +253,7 @@ WHERE NOT ifnull($action = 'DELETE', FALSE); The following section defines the main form with record fields. First the $\_valid_ids variable is constructed as the source for the drop-down id field. The code also adds the NULL value used for defining a new record. Note that, when this form is opened from the table view via the "New Record" button, the $action variable is set to "INSERT" and the id field is set to the empty array in the first assignment via the alternative UINION and to the single NULL in the second assignment. The two queries can also be combined relatively straightforwardly using CTEs. ```sql -SET $_valid_ids = ( +set _valid_ids = ( SELECT json_group_array( json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id ) @@ -263,7 +263,7 @@ SET $_valid_ids = ( SELECT '[]' WHERE $action = 'INSERT' ); -SET $_valid_ids = ( +set _valid_ids = ( json_insert($_valid_ids, '$[#]', json_object('label', 'NULL', 'value', json('null')) ) @@ -274,7 +274,7 @@ The next part defines form fields via the "dynamic" component (for some reason I Also note that this single form definition actually combines two forms (the second being the record delete confirmation form). If the $action variable is set to "DELETE" (after the delete operation is initiated from either the table or detail view), buttons are adjusted appropriately and all fields are set to read-only. Whether this is a good design is a separate question. Perhaps, defining two separate forms is a better approach. -![](https://raw.github.com/lovasoa/SQLpage/crud_auth/examples/CRUD%20-%20Authentication/www/img/delete_confirmation.png) +![](https://raw.github.com/sqlpage/SQLPage/crud_auth/examples/CRUD%20-%20Authentication/www/img/delete_confirmation.png) After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed. diff --git a/examples/CRUD - Authentication/www/css/prism-tabler-theme.css b/examples/CRUD - Authentication/www/css/prism-tabler-theme.css index 63b4611f..930a5f0b 100644 --- a/examples/CRUD - Authentication/www/css/prism-tabler-theme.css +++ b/examples/CRUD - Authentication/www/css/prism-tabler-theme.css @@ -2,43 +2,43 @@ .token.prolog, .token.doctype, .token.cdata { - color: var(--tblr-gray-300); + color: var(--tblr-gray-300); } .token.punctuation { - color: var(--tblr-gray-500); + color: var(--tblr-gray-500); } .namespace { - opacity: .7; + opacity: 0.7; } .token.property, .token.tag { - color: #f92672; + color: #f92672; - /* We need to reset the 'tag' styles set by tabler */ - border: 0; - display: inherit; - height: inherit; - border-radius: inherit; - padding: 0; - background: inherit; - box-shadow: inherit; + /* We need to reset the 'tag' styles set by tabler */ + border: 0; + display: inherit; + height: inherit; + border-radius: inherit; + padding: 0; + background: inherit; + box-shadow: inherit; } .token.number { - color: #ea9999; + color: #ea9999; } .token.boolean { - color: #ae81ff; + color: #ae81ff; } .token.selector, .token.attr-name, .token.string { - color: #97e1a3; + color: #97e1a3; } .token.operator, @@ -46,47 +46,47 @@ .token.url, .language-css .token.string, .style .token.string { - color: #f8f8f2; + color: #f8f8f2; } .token.atrule, -.token.attr-value -{ - color: #e6db74; +.token.attr-value { + color: #e6db74; } - -.token.keyword{ -color: #95d1ff; +.token.keyword { + color: #95d1ff; } .token.regex, .token.important { - color: var(--tblr-yellow); + color: var(--tblr-yellow); } .token.important { - font-weight: bold; + font-weight: bold; } .token.entity { - cursor: help; + cursor: help; } .token { - transition: .3s; + transition: 0.3s; } -code::selection, code ::selection { - background: var(--tblr-yellow); - color: var(--tblr-gray-900); - border-radius: .1em; +code::selection, +code ::selection { + background: var(--tblr-yellow); + color: var(--tblr-gray-900); + border-radius: 0.1em; } -code .token.keyword::selection, code .token.punctuation::selection { - color: var(--tblr-gray-800); +code .token.keyword::selection, +code .token.punctuation::selection { + color: var(--tblr-gray-800); } pre code { - padding: 0; -} \ No newline at end of file + padding: 0; +} diff --git a/examples/CRUD - Authentication/www/css/style.css b/examples/CRUD - Authentication/www/css/style.css index 670eef93..a6bace37 100644 --- a/examples/CRUD - Authentication/www/css/style.css +++ b/examples/CRUD - Authentication/www/css/style.css @@ -11,13 +11,13 @@ } div.dropdown-menu- { - min-width: inherit !important; + min-width: inherit !important; } a.dropdown-item- { - min-width: inherit !important; + min-width: inherit !important; } .slim_item { - min-width: inherit !important; + min-width: inherit !important; } diff --git a/examples/CRUD - Authentication/www/currencies_item_dml.sql b/examples/CRUD - Authentication/www/currencies_item_dml.sql index 3be29235..9f6f189b 100644 --- a/examples/CRUD - Authentication/www/currencies_item_dml.sql +++ b/examples/CRUD - Authentication/www/currencies_item_dml.sql @@ -7,8 +7,8 @@ -- $_curpath and $_session_required are required for header_shell_session.sql. -SET $_session_required = 1; -SET $_shell_enabled = 0; +set _session_required = 1; +set _shell_enabled = 0; SELECT 'dynamic' AS component, @@ -18,7 +18,7 @@ SELECT -- Redirect target must be passed as $path -- ============================================================================= -SET $_err_msg = '&path URL GET parameter (redirect target) is not set!'; +set _err_msg = '&path URL GET parameter (redirect target) is not set!'; SELECT 'alert' AS component, @@ -46,18 +46,18 @@ WHERE -- For new records, the id (INTEGER PRIMARY KEY AUTOINCREMENT) should be set to NULL. -- The id field is set as hidden in the record edit form and passed as the :id POST -- variable. NULL, however, cannot be passed as such and is converted to blank string. --- Check :id for '' and SET $id (:id will return the same value). +-- Check :id for '' and set id (:id will return the same value). -SET $_id = iif(typeof(:id) = 'text' AND :id = '', NULL, :id); +set _id = iif(typeof(:id) = 'text' AND :id = '', NULL, :id); -SET $_values = json_object( +set _values = json_object( 'id', CAST($_id AS INT), 'name', :name, 'to_rub', CAST(:to_rub AS NUMERIC) ); -SET $_op = iif($_id IS NULL, 'INSERT', 'UPDATE'); -SET $_err_msg = sqlpage.url_encode('New currency already in the database'); +set _op = iif($_id IS NULL, 'INSERT', 'UPDATE'); +set _err_msg = sqlpage.url_encode('New currency already in the database'); SELECT 'redirect' AS component, diff --git a/examples/CRUD - Authentication/www/currencies_item_form.sql b/examples/CRUD - Authentication/www/currencies_item_form.sql index 6eaf8f5f..435332c8 100644 --- a/examples/CRUD - Authentication/www/currencies_item_form.sql +++ b/examples/CRUD - Authentication/www/currencies_item_form.sql @@ -8,8 +8,8 @@ -- $_curpath and $_session_required are required for header_shell_session.sql. -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -19,9 +19,9 @@ SELECT -- =============================== Module vars ================================= -- ============================================================================= -SET $_getpath = '?path=' || ifnull($path, $_curpath); -SET $_action_target = 'currencies_item_dml.sql' || $_getpath; -SET $_table_list = 'currencies_list.sql'; +set _getpath = '?path=' || ifnull($path, $_curpath); +set _action_target = 'currencies_item_dml.sql' || $_getpath; +set _table_list = 'currencies_list.sql'; -- ============================================================================= -- ========================== Filter invalid $id =============================== @@ -36,7 +36,7 @@ WHERE $id = '' OR CAST($id AS INT) = 0; -- If $id is set, it must be a valid PKEY value. -SET $error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); +set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided'); SELECT 'redirect' AS component, @@ -52,7 +52,7 @@ WHERE $id NOT IN (SELECT currencies.id FROM currencies); -- -- If $values is provided, it must contain a valid JSON. -SET $_err_msg = +set _err_msg = sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __'; SELECT @@ -70,9 +70,9 @@ WHERE NOT json_valid($values); -- Field values may be provided via the $values GET variable formatted as JSON -- object. If $values contains a valid JSON, use it to populate the form. -- Otherwise, if $id is set to a valid value, retrieve the record from the --- database and set $values. If not, set $values to all NULLs. +-- database and set values. If not, set values to all NULLs. -SET $_values = ( +set _values = ( WITH fields AS ( -- If valid "id" is supplied as a GET variable, retrieve the record and @@ -136,7 +136,7 @@ WHERE NOT ifnull($action = 'DELETE', FALSE); -- passed back as POST variables, and the code above sets the $_values variable -- for proper initialization of the reloaded form. -SET $_valid_ids = ( +set _valid_ids = ( SELECT json_group_array( json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id ) @@ -146,7 +146,7 @@ SET $_valid_ids = ( SELECT '[]' WHERE $action = 'INSERT' ); -SET $_valid_ids = ( +set _valid_ids = ( json_insert($_valid_ids, '$[#]', json_object('label', 'NULL', 'value', json('null')) ) diff --git a/examples/CRUD - Authentication/www/currencies_list.sql b/examples/CRUD - Authentication/www/currencies_list.sql index 8b57841a..12839e11 100644 --- a/examples/CRUD - Authentication/www/currencies_list.sql +++ b/examples/CRUD - Authentication/www/currencies_list.sql @@ -5,8 +5,8 @@ -- $_curpath and $_session_required are required for header_shell_session.sql. -SET $_curpath = sqlpage.path(); -SET $_session_required = 1; +set _curpath = sqlpage.path(); +set _session_required = 1; SELECT 'dynamic' AS component, @@ -16,8 +16,8 @@ SELECT -- =============================== Module vars ================================= -- ============================================================================= -SET $_getpath = '&path=' || $_curpath; -SET $_item_form = 'currencies_item_form.sql'; +set _getpath = '&path=' || $_curpath; +set _item_form = 'currencies_item_form.sql'; -- ============================================================================= -- ======================== Display confirmation =============================== diff --git a/examples/CRUD - Authentication/www/header_shell_session.sql b/examples/CRUD - Authentication/www/header_shell_session.sql index 2a8c1d2f..8628d89b 100644 --- a/examples/CRUD - Authentication/www/header_shell_session.sql +++ b/examples/CRUD - Authentication/www/header_shell_session.sql @@ -21,9 +21,9 @@ -- at the top of the page script, but AFTER setting the required variables -- -- ```sql --- SET $_curpath = sqlpage.path(); --- SET $_session_required = 1; --- SET $_shell_enabled = 1; +-- set _curpath = sqlpage.path(); +-- set _session_required = 1; +-- set _shell_enabled = 1; -- ``` -- -- ## Reuired SET Variables @@ -49,9 +49,9 @@ -- Set default values (for now) for required variables. -- Probably should instead show appropriate error messages and abort rendering. -SET $_curpath = ifnull($_curpath, '/'); -SET $_session_required = ifnull($_session_required, 1); -SET $_shell_enabled = ifnull($_shell_enabled, 1); +set _curpath = ifnull($_curpath, '/'); +set _session_required = ifnull($_session_required, 1); +set _shell_enabled = ifnull($_shell_enabled, 1); -- ============================================================================= -- ========================= Check active session ============================== @@ -60,7 +60,7 @@ SET $_shell_enabled = ifnull($_shell_enabled, 1); -- Check if session is available. -- Require the user to log in again after 1 day -SET $_username = ( +set _username = ( SELECT username FROM sessions WHERE sqlpage.cookie('session_token') = id @@ -69,7 +69,7 @@ SET $_username = ( -- Redirect to the login page if the user is not logged in. -- Unprotected pages must --- SET $_session_required = (SELECT FALSE); +-- set _session_required = (SELECT FALSE); -- before running this script SELECT diff --git a/examples/CRUD - Authentication/www/intro.sql b/examples/CRUD - Authentication/www/intro.sql index bcb5857c..eb9219a4 100644 --- a/examples/CRUD - Authentication/www/intro.sql +++ b/examples/CRUD - Authentication/www/intro.sql @@ -5,8 +5,8 @@ -- $_curpath and $_session_required are required for header_shell_session.sql. -SET $_curpath = sqlpage.path(); -SET $_session_required = 0; +set _curpath = sqlpage.path(); +set _session_required = 0; SELECT 'dynamic' AS component, diff --git a/examples/CRUD - Authentication/www/login.sql b/examples/CRUD - Authentication/www/login.sql index 2b9a6a3b..f7259316 100644 --- a/examples/CRUD - Authentication/www/login.sql +++ b/examples/CRUD - Authentication/www/login.sql @@ -1,6 +1,6 @@ -- Authentication Fence -SET $username = ( +set username = ( SELECT username FROM sessions WHERE sqlpage.cookie('session_token') = id diff --git a/examples/CRUD - Authentication/www/menu_test/dummy.sql b/examples/CRUD - Authentication/www/menu_test/dummy.sql index a3a32e92..92619517 100644 --- a/examples/CRUD - Authentication/www/menu_test/dummy.sql +++ b/examples/CRUD - Authentication/www/menu_test/dummy.sql @@ -4,14 +4,14 @@ select 'database' as icon, '/' as link, 'Top' as menu_item, - '{"title":"About","submenu":[{"link":"/safety.sql","title":"Security"},{"link":"/performance.sql","title":"Performance"},{"link":"//github.com/lovasoa/SQLpage/blob/main/LICENSE.txt","title":"License"},{"link":"/blog.sql","title":"Articles"}]}' as menu_item, + '{"title":"About","submenu":[{"link":"/safety.sql","title":"Security"},{"link":"/performance.sql","title":"Performance"},{"link":"//github.com/sqlpage/SQLPage/blob/main/LICENSE.txt","title":"License"},{"link":"/blog.sql","title":"Articles"}]}' as menu_item, NULL as menu_item, - '{"title":"Examples","submenu":[{"link":"/examples/tabs.sql","title":"Tabs"},{"link":"/examples/layouts.sql","title":"Layouts"},{"link":"/examples/multistep-form","title":"Forms"},{"link":"/examples/handle_picture_upload.sql","title":"File uploads"},{"link":"/examples/hash_password.sql","title":"Password protection"},{"link":"//github.com/lovasoa/SQLpage/blob/main/examples/","title":"All examples & demos"}]}' as menu_item, + '{"title":"Examples","submenu":[{"link":"/examples/tabs/","title":"Tabs"},{"link":"/examples/layouts.sql","title":"Layouts"},{"link":"/examples/multistep-form","title":"Forms"},{"link":"/examples/handle_picture_upload.sql","title":"File uploads"},{"link":"/examples/hash_password.sql","title":"Password protection"},{"link":"//github.com/sqlpage/SQLPage/blob/main/examples/","title":"All examples & demos"}]}' as menu_item, '{"title":"z", "icon": "settings"}' as menu_item, '{"title":"", "icon": ""}' as menu_item, - '{"title":"Community","submenu":[{"link":"blog.sql","title":"Blog"},{"link":"//github.com/lovasoa/sqlpage/issues","title":"Report a bug"},{"link":"//github.com/lovasoa/sqlpage/discussions","title":"Discussions"},{"link":"//github.com/lovasoa/sqlpage","title":"Github"}]}' as menu_item, + '{"title":"Community","submenu":[{"link":"blog.sql","title":"Blog"},{"link":"//github.com/sqlpage/SQLPage/issues","title":"Report a bug"},{"link":"//github.com/sqlpage/SQLPage/discussions","title":"Discussions"},{"link":"//github.com/sqlpage/SQLPage","title":"Github"}]}' as menu_item, NULL as menu_item, - '{"title":"Documentation","submenu":[{"link":"/your-first-sql-website","title":"Getting started"},{"link":"/components.sql","title":"All Components"},{"link":"/functions.sql","title":"SQLPage Functions"},{"link":"/custom_components.sql","title":"Custom Components"},{"link":"//github.com/lovasoa/SQLpage/blob/main/configuration.md#configuring-sqlpage","title":"Configuration"}]}' as menu_item, + '{"title":"Documentation","submenu":[{"link":"/your-first-sql-website","title":"Getting started"},{"link":"/components.sql","title":"All Components"},{"link":"/functions.sql","title":"SQLPage Functions"},{"link":"/custom_components.sql","title":"Custom Components"},{"link":"//github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage","title":"Configuration"}]}' as menu_item, 'boxed' as layout, 'en-US' as language, 'Documentation for the SQLPage low-code web application framework.' as description, @@ -19,4 +19,4 @@ select 'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js' as javascript, 'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js' as javascript, '/prism-tabler-theme.css' as css, - 'Official [SQLPage](https://sql.ophir.dev) documentation' as footer; \ No newline at end of file + 'Official [SQLPage](https://sql-page.com) documentation' as footer; \ No newline at end of file diff --git a/examples/CRUD - Authentication/www/menu_test/dummy_menu.sql b/examples/CRUD - Authentication/www/menu_test/dummy_menu.sql index 97c13fdb..2ce87ff7 100644 --- a/examples/CRUD - Authentication/www/menu_test/dummy_menu.sql +++ b/examples/CRUD - Authentication/www/menu_test/dummy_menu.sql @@ -1,4 +1,4 @@ -SET $_get_vars = ( +set _get_vars = ( SELECT json_group_object( "key", @@ -10,10 +10,10 @@ SET $_get_vars = ( ); -SET $_locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn' -SET $_theme = 'fancy'; --$theme; -- 'default', 'fancy' -SET $_hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN) -SET $_authenticated = $authenticated; -- 0, 1 (BOOLEAN) +set _locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn' +set _theme = 'fancy'; --$theme; -- 'default', 'fancy' +set _hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN) +set _authenticated = $authenticated; -- 0, 1 (BOOLEAN) -- ============================================================================= -- ============================================================================= diff --git a/examples/CRUD - Authentication/www/menu_test/menu_code.sql b/examples/CRUD - Authentication/www/menu_test/menu_code.sql index a843a031..4d7d8f70 100644 --- a/examples/CRUD - Authentication/www/menu_test/menu_code.sql +++ b/examples/CRUD - Authentication/www/menu_test/menu_code.sql @@ -1,7 +1,7 @@ --- SET $_locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn' --- SET $_theme = $theme; -- 'default', 'fancy' --- SET $_hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN) --- SET $_authenticated = $authenticated; -- 0, 1 (BOOLEAN) +-- set _locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn' +-- set _theme = $theme; -- 'default', 'fancy' +-- set _hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN) +-- set _authenticated = $authenticated; -- 0, 1 (BOOLEAN) -- ============================================================================= -- ============================================================================= diff --git a/examples/CRUD - Authentication/www/menu_test/menu_demo.sql b/examples/CRUD - Authentication/www/menu_test/menu_demo.sql index d5c7e6a5..5d34d865 100644 --- a/examples/CRUD - Authentication/www/menu_test/menu_demo.sql +++ b/examples/CRUD - Authentication/www/menu_test/menu_demo.sql @@ -68,7 +68,7 @@ select 'dynamic' as component, "icon": "trash", "submenu": [ { - "link": "/examples/tabs.sql", + "link": "/examples/tabs/", "icon": "device-floppy", "title": "Tabs" }, @@ -93,7 +93,7 @@ select 'dynamic' as component, "title": "Blog" }, { - "link": "//github.com/lovasoa/sqlpage/issues", + "link": "//github.com/sqlpage/SQLPage/issues", "title": "Report a bug" } ] diff --git a/examples/PostGIS - using sqlpage with geographic data/README.md b/examples/PostGIS - using sqlpage with geographic data/README.md index c840e300..0ba101a8 100644 --- a/examples/PostGIS - using sqlpage with geographic data/README.md +++ b/examples/PostGIS - using sqlpage with geographic data/README.md @@ -3,10 +3,10 @@ ## Introduction This is a small application that uses [PostGIS](https://postgis.net/) -to save data associated to greographic coordinates. +to save data associated with geographic coordinates. -If you are using a SQLite database instead see [this other example instead](../make%20a%20geographic%20data%20application%20using%20sqlite%20extensions/), -which implements the same application using the `spatialite` extension. +If you are using a SQLite database, see [this other example instead](../make%20a%20geographic%20data%20application%20using%20sqlite%20extensions/), +which implements the same application using the `spatialite` extension for SQLite. ### Installation diff --git a/examples/PostGIS - using sqlpage with geographic data/sqlpage/sqlpage.json b/examples/PostGIS - using sqlpage with geographic data/sqlpage/sqlpage.json index a0806999..5216cfac 100644 --- a/examples/PostGIS - using sqlpage with geographic data/sqlpage/sqlpage.json +++ b/examples/PostGIS - using sqlpage with geographic data/sqlpage/sqlpage.json @@ -1,3 +1,3 @@ { - "database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database" -} \ No newline at end of file + "database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database" +} diff --git a/examples/SQLPage developer user interface/docker-compose.yml b/examples/SQLPage developer user interface/docker-compose.yml index 8339d5b6..7535016c 100644 --- a/examples/SQLPage developer user interface/docker-compose.yml +++ b/examples/SQLPage developer user interface/docker-compose.yml @@ -1,6 +1,6 @@ services: web: - image: lovasoa/sqlpage:main # main is cutting edge, use lovasoa/sqlpage:latest for the latest stable version + image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version ports: - "8080:8080" volumes: diff --git a/examples/SQLPage developer user interface/sqlpage/sqlpage.json b/examples/SQLPage developer user interface/sqlpage/sqlpage.json index 237588e9..6759cb60 100644 --- a/examples/SQLPage developer user interface/sqlpage/sqlpage.json +++ b/examples/SQLPage developer user interface/sqlpage/sqlpage.json @@ -1,3 +1,3 @@ { - "content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;" -} \ No newline at end of file + "content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;" +} diff --git a/examples/SQLPage developer user interface/website/js/code-editor.js b/examples/SQLPage developer user interface/website/js/code-editor.js index 7686754d..e8c35f54 100644 --- a/examples/SQLPage developer user interface/website/js/code-editor.js +++ b/examples/SQLPage developer user interface/website/js/code-editor.js @@ -1,34 +1,34 @@ -const cdn = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/'; +const cdn = "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/"; function on_monaco_load() { - // Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea - // When the form is submitted, set the value of the textarea to the value of the Monaco editor - const textarea = document.getElementById('code-editor'); - const editorDiv = document.createElement('div'); - editorDiv.style.width = '100%'; - editorDiv.style.height = '700px'; - textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling); - const monacoConfig = { - value: textarea.value, - language: 'sql', - }; + // Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea + // When the form is submitted, set the value of the textarea to the value of the Monaco editor + const textarea = document.getElementById("code-editor"); + const editorDiv = document.createElement("div"); + editorDiv.style.width = "100%"; + editorDiv.style.height = "700px"; + textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling); + const monacoConfig = { + value: textarea.value, + language: "sql", + }; - self.MonacoEnvironment = { - baseUrl: cdn + 'min/', - }; - const editor = monaco.editor.create(editorDiv, monacoConfig); - textarea.style.display = 'none'; - const form = textarea.form; - form.addEventListener('submit', () => { - textarea.value = editor.getValue(); - }); + self.MonacoEnvironment = { + baseUrl: `${cdn}min/`, + }; + const editor = monaco.editor.create(editorDiv, monacoConfig); + textarea.style.display = "none"; + const form = textarea.form; + form.addEventListener("submit", () => { + textarea.value = editor.getValue(); + }); } function set_require_config() { - require.config({ paths: { vs: cdn + 'min/vs' } }); - require(['vs/editor/editor.main'], on_monaco_load); + require.config({ paths: { vs: `${cdn}min/vs` } }); + require(["vs/editor/editor.main"], on_monaco_load); } -const loader_script = document.createElement('script'); -loader_script.src = cdn + 'min/vs/loader.js'; +const loader_script = document.createElement("script"); +loader_script.src = `${cdn}min/vs/loader.js`; loader_script.onload = set_require_config; -document.head.appendChild(loader_script); \ No newline at end of file +document.head.appendChild(loader_script); diff --git a/examples/charts, computations and custom components/index.sql b/examples/charts, computations and custom components/index.sql index b58a4441..eca56fa7 100644 --- a/examples/charts, computations and custom components/index.sql +++ b/examples/charts, computations and custom components/index.sql @@ -5,7 +5,7 @@ SELECT 'shell' AS component, 'A tool to measure a tempo in bpm by clicking a button in rythm.' as description, 'Vollkorn' as font, 'music' as icon, - 'Proudly powered by [SQLPage](https://sql.ophir.dev)' as footer; + 'Proudly powered by [SQLPage](https://sql-page.com)' as footer; SELECT 'hero' as component, 'Tap Tempo' as title, @@ -19,7 +19,7 @@ SELECT 'text' as component, ' ## Context -This tool is written in the SQL database query language, and uses the [SQLPage](https://sql.ophir.dev) framework to generate the web interface. +This tool is written in the SQL database query language, and uses the [SQLPage](https://sql-page.com) framework to generate the web interface. It serves as a demo for the framework. diff --git a/examples/corporate-conundrum/game.sql b/examples/corporate-conundrum/game.sql index 9c8976d2..ab620aa9 100644 --- a/examples/corporate-conundrum/game.sql +++ b/examples/corporate-conundrum/game.sql @@ -3,7 +3,7 @@ select * FROM sqlpage_shell; -- Display the list of players with a link for each one to start playing INSERT INTO players(name, game_id) SELECT $Player as name, - $id::integer as game_id + CAST($id AS INTEGER) as game_id WHERE $Player IS NOT NULL; SELECT 'list' as component, @@ -17,7 +17,7 @@ SELECT name as title, ) ) as link FROM players -WHERE game_id = $id::integer; +WHERE game_id = CAST($id AS INTEGER); --------------------------- -- Player insertion form -- --------------------------- @@ -35,7 +35,7 @@ INSERT INTO game_questions( impostor, game_order ) -SELECT $id::integer as game_id, +SELECT CAST($id AS INTEGER) as game_id, questions.id as question_id, -- When the true answer is small, set the wrong answer to just +/- 1, otherwise -25%/+75%. -- When it is a date between 1200 and 2100, make it -25 % or +75 % of the distance to today @@ -50,8 +50,8 @@ SELECT $id::integer as game_id, random() as game_order FROM questions LEFT JOIN game_questions ON questions.id = game_questions.question_id - AND game_questions.game_id = $id::integer + AND game_questions.game_id = CAST($id AS INTEGER) WHERE game_questions.question_id IS NULL AND $Player IS NOT NULL ORDER BY random() -LIMIT 1; \ No newline at end of file +LIMIT 1; diff --git a/examples/corporate-conundrum/wait.sql b/examples/corporate-conundrum/wait.sql index 8880ef05..c6124823 100644 --- a/examples/corporate-conundrum/wait.sql +++ b/examples/corporate-conundrum/wait.sql @@ -1,8 +1,8 @@ -- Redirect to the next question when all players have answered set page_params = json_object('game_id', $game_id, 'player', $player); select CASE - (SELECT count(*) FROM answers WHERE question_id = $question_id AND game_id = $game_id::integer) - WHEN (SELECT count(*) FROM players WHERE game_id = $game_id::integer) + (SELECT count(*) FROM answers WHERE question_id = $question_id AND game_id = CAST($game_id AS INTEGER)) + WHEN (SELECT count(*) FROM players WHERE game_id = CAST($game_id AS INTEGER)) THEN '0; ' || sqlpage.link('next-question.sql', $page_params) ELSE 3 END as refresh, @@ -11,10 +11,10 @@ FROM sqlpage_shell; -- Insert the answer into the answers table INSERT INTO answers(game_id, player_name, question_id, answer_value) -SELECT $game_id::integer as game_id, +SELECT CAST($game_id AS INTEGER) as game_id, $player as player_name, - $question_id::integer as question_id, - $answer::integer as answer_value + CAST($question_id AS INTEGER) as question_id, + CAST($answer AS INTEGER) as answer_value WHERE $answer IS NOT NULL; -- Redirect to the next question SELECT 'text' as component, @@ -22,11 +22,11 @@ SELECT 'text' as component, select group_concat(name, ', ') as contents, TRUE as bold from players -where game_id = $game_id::integer +where game_id = CAST($game_id AS INTEGER) and not EXISTS ( SELECT 1 FROM answers - WHERE answers.game_id = $game_id::integer + WHERE answers.game_id = CAST($game_id AS INTEGER) AND answers.player_name = players.name - AND answers.question_id = $question_id::integer - ); \ No newline at end of file + AND answers.question_id = CAST($question_id AS INTEGER) + ); diff --git a/examples/custom form component/README.md b/examples/custom form component/README.md new file mode 100644 index 00000000..7f3d7c15 --- /dev/null +++ b/examples/custom form component/README.md @@ -0,0 +1,19 @@ +# Custom form component + +This example shows how to create a simple custom component in handlebars, and call it from SQL. + +It uses MySQL, but it should be easy to adapt to other databases. +The only MySQL-specific features used here are: + - `json_table`, which is supported by MariaDB and MySQL 8.0 and later, + - MySQL's `json_merge` function. + +Both [have analogs in other databases](https://sql-page.com/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). + +![screenshot](screenshot.png) + + +## Key features illustrated in this example + +- How to create a custom component in handlebars, with dynamic behavior implemented in JavaScript +- How to manage multiple-option select boxes, with pre-selected items, and multiple choices +- Including a common menu between different pages using a `shell.sql` file, the dynamic component, and the `sqlpage.run_sql` function. diff --git a/examples/custom form component/basic.sql b/examples/custom form component/basic.sql new file mode 100644 index 00000000..77f8b298 --- /dev/null +++ b/examples/custom form component/basic.sql @@ -0,0 +1,40 @@ +select + 'dynamic' as component, + sqlpage.run_sql ('shell.sql') as properties; + +-- this does the same thing as index.sql, but uses the normal form component instead of our fancy dual-list component +select + 'form' as component, + 'form_action.sql' as action; + +select + 'select' as type, + true as searchable, + true as multiple, + 'selected_items[]' as name, + 'Users in this group' as label, + -- JSON_MERGE combines two JSON documents: + -- 1. A JSON object with an empty label + -- 2. An array of user objects created by JSON_ARRAYAGG + JSON_MERGE ( + -- Creates a simple JSON object with a single empty property {"label": ""} + JSON_OBJECT ('label', ''), + -- JSON_ARRAYAGG takes multiple rows and combines them into a JSON array + -- Each element in the array is a JSON object created by json_object() + JSON_ARRAYAGG ( + -- Creates a JSON object for each user with: + -- - {"label": "the user's name", "value": "the user's ID", "selected": true } (if the user is in the group) + json_object ( + 'label', + users.name, + 'value', + users.id, + 'selected', + group_members.group_id is not null -- the left join creates NULLs for users not in the group + ) + ) + ) as options +from + users + left join group_members on users.id = group_members.user_id + and group_members.group_id = 1; \ No newline at end of file diff --git a/examples/custom form component/docker-compose.yml b/examples/custom form component/docker-compose.yml new file mode 100644 index 00000000..284c87c1 --- /dev/null +++ b/examples/custom form component/docker-compose.yml @@ -0,0 +1,19 @@ +services: + web: + image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version + ports: + - "8080:8080" + volumes: + - .:/var/www + - ./sqlpage:/etc/sqlpage + depends_on: + - db + environment: + DATABASE_URL: mysql://root:secret@db/sqlpage + db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases + ports: + - "3306:3306" + image: mysql:9 # support for json_table was added in mariadb 10.6 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: sqlpage \ No newline at end of file diff --git a/examples/custom form component/form_action.sql b/examples/custom form component/form_action.sql new file mode 100644 index 00000000..bb71cd3f --- /dev/null +++ b/examples/custom form component/form_action.sql @@ -0,0 +1,28 @@ +-- remove all existing members from this group +delete from group_members where group_id = 1; + +-- add the selected members to this group +-- This query takes a JSON array and converts it to rows +-- :selected_items contains a JSON array of user IDs, e.g. ["1", "2", "3"], generated by SQLPage from the multiple-select box answers posted by the browser +-- json_table breaks down the JSON array into individual rows +-- '$[*]' means "look at each element in the root array" +-- columns (id int path '$') extracts each array element as an integer into a column named 'id' +-- The result is a temporary table with one integer column (id) and one row per array element +insert into group_members (group_id, user_id) +select 1, id +from json_table( + :selected_items, + '$[*]' columns (id int path '$') +) as submitted_items; + +select 'alert' as component, 'Group members successfully updated !' as title, 'success' as color; + +select 'list' as component, 'Users in this group' as title; + +select name as title, email as description +from users +join group_members on users.id = group_members.user_id +where group_members.group_id = 1; + +select 'button' as component; +select 'Go back' as title, 'index.sql' as link; \ No newline at end of file diff --git a/examples/custom form component/index.sql b/examples/custom form component/index.sql new file mode 100644 index 00000000..4cd48d92 --- /dev/null +++ b/examples/custom form component/index.sql @@ -0,0 +1,17 @@ +-- include the common menu +select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; + +-- Call our custom component from ./sqlpage/templates/dual-list.handlebars +select + 'dual-list' as component, + 'form_action.sql' as action; + +-- This SQL query returns the list of users, with a boolean indicating if they are in the group +select + id, + name as label, + group_members.group_id is not null as selected +from + users + left join group_members on users.id = group_members.user_id + and group_members.group_id = 1; \ No newline at end of file diff --git a/examples/custom form component/screenshot.png b/examples/custom form component/screenshot.png new file mode 100644 index 00000000..dc41e846 Binary files /dev/null and b/examples/custom form component/screenshot.png differ diff --git a/examples/custom form component/shell.sql b/examples/custom form component/shell.sql new file mode 100644 index 00000000..580a4fe9 --- /dev/null +++ b/examples/custom form component/shell.sql @@ -0,0 +1,5 @@ +select + 'shell' as component, + 'Custom form component' as title, + 'index' as menu_item, + 'basic' as menu_item; \ No newline at end of file diff --git a/examples/custom form component/sqlpage/migrations/0001_users_and_groups.sql b/examples/custom form component/sqlpage/migrations/0001_users_and_groups.sql new file mode 100644 index 00000000..1409f7a1 --- /dev/null +++ b/examples/custom form component/sqlpage/migrations/0001_users_and_groups.sql @@ -0,0 +1,40 @@ +create table users ( + id int primary key auto_increment, + name varchar(255) not null, + email varchar(255) not null +); + +create table `groups` ( + id int primary key auto_increment, + name varchar(255) not null +); + +create table group_members ( + group_id int not null, + user_id int not null, + primary key (group_id, user_id), + foreign key (group_id) references `groups` (id), + foreign key (user_id) references users (id) +); + +INSERT INTO users (id, name, email) VALUES +(1, 'John Smith', 'john@email.com'), +(2, 'Jane Doe', 'jane@email.com'), +(3, 'Bob Wilson', 'bob@email.com'), +(4, 'Mary Johnson', 'mary@email.com'), +(5, 'James Brown', 'james@email.com'), +(6, 'Sarah Davis', 'sarah@email.com'), +(7, 'Michael Lee', 'michael@email.com'), +(8, 'Lisa Anderson', 'lisa@email.com'), +(9, 'David Miller', 'david@email.com'), +(10, 'Emma Wilson', 'emma@email.com'); + +INSERT INTO `groups` (id, name) VALUES +(1, 'Team Alpha'); + +INSERT INTO group_members (group_id, user_id) VALUES +(1, 1), +(1, 2), +(1, 3), +(1, 4), +(1, 5); \ No newline at end of file diff --git a/examples/custom form component/sqlpage/templates/dual-list.handlebars b/examples/custom form component/sqlpage/templates/dual-list.handlebars new file mode 100644 index 00000000..ce103c04 --- /dev/null +++ b/examples/custom form component/sqlpage/templates/dual-list.handlebars @@ -0,0 +1,126 @@ +{{!-- This is a form that will post data to the URL specified in the top-level 'action' property coming from the SQL query --}} +
+ {{!-- Create a row with centered content and spacing between items --}} +
+ {{!-- Left List Box: 5 columns wide (out of the 12 made available by bootstrap) --}} +
+ {{!-- Card with no border and subtle shadow --}} +
+ {{!-- Card header with white background, no border, semibold font, and secondary text color --}} +
+ Available Items +
+
+ {{!-- Multiple-select dropdown list, 300px tall --}} + +
+
+
+ + {{!-- Middle section with transfer buttons (auto-sized column) --}} +
+ {{!-- Right arrow button (→) to move items to selected list --}} + + {{!-- Left arrow button (←) to remove items from selected list --}} + +
+ + {{!-- Right List Box (5 columns wide) --}} +
+
+
+ Selected Items +
+
+ {{!-- Multiple-select dropdown that will contain selected items. The name attribute makes it submit as an array --}} + +
+
+
+ + {{!-- Submit Button Section (full width) --}} +
+ +
+
+
+ +{{!-- JavaScript code with CSP (Content Security Policy) nonce for security --}} + diff --git a/examples/forms-with-multiple-steps/README.md b/examples/forms-with-multiple-steps/README.md new file mode 100644 index 00000000..a742ff52 --- /dev/null +++ b/examples/forms-with-multiple-steps/README.md @@ -0,0 +1,64 @@ +# Forms with multiple steps + +Multi-steps forms are forms where the user has to go through multiple pages +to fill in all the information. +They are a good practice to improve the user experience +on complex forms by removing the cognitive load of filling in a long form at once. +Additionally, they allow you to validate the input at each step, +and create dynamic forms, where the next step depends on the user's input. + +There are multiple ways to create forms with multiple steps in SQLPage, +which vary in the way the state of the partially filled form +is persisted between steps. + +This example illustrates the main ones. +All the examples will implement the same simple form: +a form that asks for a person's name, email, and age. + +## [Storing the state in hidden fields](./hidden/) + +![schema](./hidden/illustration.png) + +You can store the state of the partially filled form in hidden fields, +using `'hidden' as type` in the [form component](https://sql-page.com/component.sql?component=form#component). + + - **advantages** + - simple to implement + - the form state is not sent to the server when the user navigates to other pages + - **disadvantages** + - the entire state is re-sent to the server on each step + - you need to reference all the previous answers in each step + - no *backwards navigation*: the user has to fill in the steps in order. If they go back to a previous step, you cannot prefill the form with the previous answers, or save the data they have already entered. + +## [Storing the state in the database](./database/) + +You can store the state of the partially filled form in the database, +either in the final table where you want to store the data, +or in a dedicated table that will be used to store only partial data, +allowing you to have more relaxed column constraints in the partially filled data. + + - **advantages** + - the website administrator can access user inputs before they submit the final form + - the user can start filling the form on one device, and continue on another one. + - the user can have multiple partially filled forms in flight at the same time. + - **disadvantages** + - the website administrator needs to manage a dedicated table for the form state + - old partially filled forms may pile up in the database + +## [Storing the state in cookies](./cookies/) + +You can store each answer of the user in a cookie, +using the +[`cookie` component](https://sql-page.com/component.sql?component=cookie#component). +and retrieve it on the next step using the +[`sqlpage.cookie` function](https://sql-page.com/functions.sql?function=cookie#function). + + - **advantages** + - simple to implement + - if the user leaves the form before submitting it, and returns to it later, + the state will be persisted. + - works even if some of the steps do not use the form component. + - **disadvantages** + - the entire state is re-sent to the server on each step + - the user needs to have cookies enabled to fill in the form + - if the user leaves the form before submitting it, the form state will keep being sent to all the pages he visits until he submits the form. \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/cookies/finish.sql b/examples/forms-with-multiple-steps/cookies/finish.sql new file mode 100644 index 00000000..7aa66f10 --- /dev/null +++ b/examples/forms-with-multiple-steps/cookies/finish.sql @@ -0,0 +1,20 @@ +insert into users ( + name, email, age +) values ( + sqlpage.cookie('name'), + sqlpage.cookie('email'), + :age -- This is the age that was submitted from the form in step_3.sql +); + +-- remove cookies +with t(name) as (values ('name'), ('email'), ('age')) +select 'cookie' as component, name, '/cookies/' as path, true as remove from t; + +select + 'alert' as component, + 'Welcome, ' || name || '!' as title, + 'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md +from users where id = last_insert_rowid(); + +select 'list' as component, 'Existing users' as title, 'users' as value; +select name as title, email as description from users; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/cookies/index.sql b/examples/forms-with-multiple-steps/cookies/index.sql new file mode 100644 index 00000000..e4d419af --- /dev/null +++ b/examples/forms-with-multiple-steps/cookies/index.sql @@ -0,0 +1 @@ +select 'redirect' as component, 'step_1.sql' as link; diff --git a/examples/forms-with-multiple-steps/cookies/step_1.sql b/examples/forms-with-multiple-steps/cookies/step_1.sql new file mode 100644 index 00000000..ad9a2ad7 --- /dev/null +++ b/examples/forms-with-multiple-steps/cookies/step_1.sql @@ -0,0 +1,8 @@ +select + 'form' as component, + 'step_2.sql' as action; + +select + 'name' as name, + true as required, + sqlpage.cookie ('name') as value; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/cookies/step_2.sql b/examples/forms-with-multiple-steps/cookies/step_2.sql new file mode 100644 index 00000000..ebb531f3 --- /dev/null +++ b/examples/forms-with-multiple-steps/cookies/step_2.sql @@ -0,0 +1,17 @@ +select + 'cookie' as component, + 'name' as name, + :name as value, + '/cookies/' as path; -- Only send the cookie for pages in the /cookies/ directory + +select + 'form' as component, + 'step_3.sql' as action; + +select + 'email' as name, + 'email' as type, + true as required, + sqlpage.cookie ('email') as value, + 'you@example.com' as placeholder, + 'Hey ' || coalesce(:name, sqlpage.cookie('name')) || '! what is your email?' as description; diff --git a/examples/forms-with-multiple-steps/cookies/step_3.sql b/examples/forms-with-multiple-steps/cookies/step_3.sql new file mode 100644 index 00000000..57b960d5 --- /dev/null +++ b/examples/forms-with-multiple-steps/cookies/step_3.sql @@ -0,0 +1,15 @@ +select + 'cookie' as component, + 'email' as name, + :email as value, + '/cookies/' as path; + +select + 'form' as component, + 'finish.sql' as action; + +select + 'age' as name, + 'number' as type, + true as required, + 'How old are you, ' || sqlpage.cookie('name') || '?' as description; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/database/finish.sql b/examples/forms-with-multiple-steps/database/finish.sql new file mode 100644 index 00000000..40dfc707 --- /dev/null +++ b/examples/forms-with-multiple-steps/database/finish.sql @@ -0,0 +1,16 @@ +update partially_filled_users set age = :age +where :age is not null and id = $id; + +insert into users (name, email, age) +select name, email, age from partially_filled_users where id = $id; + +delete from partially_filled_users where id = $id; + +select + 'alert' as component, + 'Welcome, ' || name || '!' as title, + 'You are user #' || id || '. [Create a new user](index.sql)' as description_md +from users where id = last_insert_rowid(); + +select 'list' as component, 'Existing users' as title, 'users' as value; +select name as title, email as description from users; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/database/index.sql b/examples/forms-with-multiple-steps/database/index.sql new file mode 100644 index 00000000..eba04a4c --- /dev/null +++ b/examples/forms-with-multiple-steps/database/index.sql @@ -0,0 +1,5 @@ +-- create a new empty partially_filled_users row, returning its id +insert into partially_filled_users default values +returning + 'redirect' as component, + 'step_1.sql?id=' || id as link; diff --git a/examples/forms-with-multiple-steps/database/step_1.sql b/examples/forms-with-multiple-steps/database/step_1.sql new file mode 100644 index 00000000..97c492b8 --- /dev/null +++ b/examples/forms-with-multiple-steps/database/step_1.sql @@ -0,0 +1,4 @@ +select 'form' as component, 'step_2.sql?id=' || $id as action; + +select 'name' as name, true as required, name as value +from partially_filled_users where id = $id; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/database/step_2.sql b/examples/forms-with-multiple-steps/database/step_2.sql new file mode 100644 index 00000000..68b47820 --- /dev/null +++ b/examples/forms-with-multiple-steps/database/step_2.sql @@ -0,0 +1,9 @@ +update partially_filled_users set name = :name +where :name is not null and id = $id; + +select 'form' as component, 'step_3.sql?id=' || $id as action; + +select 'email' as name, 'email' as type, true as required, email as value, + 'you@example.com' as placeholder, + 'Hey ' || name || '! what is your email?' as description +from partially_filled_users where id = $id; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/database/step_3.sql b/examples/forms-with-multiple-steps/database/step_3.sql new file mode 100644 index 00000000..f4b27620 --- /dev/null +++ b/examples/forms-with-multiple-steps/database/step_3.sql @@ -0,0 +1,8 @@ +update partially_filled_users set email = :email +where :email is not null and id = $id; + +select 'form' as component, 'finish.sql?id=' || $id as action; + +select 'age' as name, 'number' as type, true as required, age as value, + 'How old are you, ' || name || '?' as description +from partially_filled_users where id = $id; diff --git a/examples/forms-with-multiple-steps/hidden/finish.sql b/examples/forms-with-multiple-steps/hidden/finish.sql new file mode 100644 index 00000000..7a34bf3e --- /dev/null +++ b/examples/forms-with-multiple-steps/hidden/finish.sql @@ -0,0 +1,8 @@ +insert into users (name, email, age) values (:name, :email, :age) +returning + 'alert' as component, + 'Welcome, ' || name || '!' as title, + 'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md; + +select 'list' as component, 'Existing users' as title, 'users' as value; +select name as title, email as description from users; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/hidden/illustration.png b/examples/forms-with-multiple-steps/hidden/illustration.png new file mode 100644 index 00000000..8cee2547 Binary files /dev/null and b/examples/forms-with-multiple-steps/hidden/illustration.png differ diff --git a/examples/forms-with-multiple-steps/hidden/index.sql b/examples/forms-with-multiple-steps/hidden/index.sql new file mode 100644 index 00000000..e4d419af --- /dev/null +++ b/examples/forms-with-multiple-steps/hidden/index.sql @@ -0,0 +1 @@ +select 'redirect' as component, 'step_1.sql' as link; diff --git a/examples/forms-with-multiple-steps/hidden/step_1.sql b/examples/forms-with-multiple-steps/hidden/step_1.sql new file mode 100644 index 00000000..fbaefa18 --- /dev/null +++ b/examples/forms-with-multiple-steps/hidden/step_1.sql @@ -0,0 +1,7 @@ +select + 'form' as component, + 'step_2.sql' as action; + +select + 'name' as name, + true as required; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/hidden/step_2.sql b/examples/forms-with-multiple-steps/hidden/step_2.sql new file mode 100644 index 00000000..24e4ac4c --- /dev/null +++ b/examples/forms-with-multiple-steps/hidden/step_2.sql @@ -0,0 +1,13 @@ +select + 'form' as component, + 'step_3.sql' as action; + +select + 'email' as name, + 'email' as type, + true as required, + 'you@example.com' as placeholder, + 'Hey ' || :name || '! what is your email?' as description; + +with previous_answers(name, value) as (values ('name', :name)) +select 'hidden' as type, name, value from previous_answers; diff --git a/examples/forms-with-multiple-steps/hidden/step_3.sql b/examples/forms-with-multiple-steps/hidden/step_3.sql new file mode 100644 index 00000000..d2883a72 --- /dev/null +++ b/examples/forms-with-multiple-steps/hidden/step_3.sql @@ -0,0 +1,12 @@ +select + 'form' as component, + 'finish.sql' as action; + +select + 'age' as name, + 'number' as type, + true as required, + 'How old are you, ' || :name || '?' as description; + +with previous_answers(name, value) as (values ('name', :name), ('email', :email)) +select 'hidden' as type, name, value from previous_answers; diff --git a/examples/forms-with-multiple-steps/index.sql b/examples/forms-with-multiple-steps/index.sql new file mode 100644 index 00000000..0f743785 --- /dev/null +++ b/examples/forms-with-multiple-steps/index.sql @@ -0,0 +1,7 @@ +select 'list' as component, 'Forms with multiple steps' as title; + +select 'Database persistence' as title, 'database' as link; +select 'Cookies' as title, 'cookies' as link; +select 'Hidden fields' as title, 'hidden' as link; + +select 'text' as component, sqlpage.read_file_as_text('README.md') as contents_md; \ No newline at end of file diff --git a/examples/forms-with-multiple-steps/sqlpage/migrations/01_users.sql b/examples/forms-with-multiple-steps/sqlpage/migrations/01_users.sql new file mode 100644 index 00000000..f902c39c --- /dev/null +++ b/examples/forms-with-multiple-steps/sqlpage/migrations/01_users.sql @@ -0,0 +1,7 @@ +-- Simple SQLite users table +create table users ( + id integer primary key autoincrement, + name text not null, + email text not null, + age integer not null check(age > 0) +); diff --git a/examples/forms-with-multiple-steps/sqlpage/migrations/02_database_persistence.sql b/examples/forms-with-multiple-steps/sqlpage/migrations/02_database_persistence.sql new file mode 100644 index 00000000..b5afd156 --- /dev/null +++ b/examples/forms-with-multiple-steps/sqlpage/migrations/02_database_persistence.sql @@ -0,0 +1,7 @@ +-- this table will store partially filled user forms +create table partially_filled_users ( + id integer primary key autoincrement, + name text null, -- all fields are nullable, because the user may not have filled them yet + email text null, + age integer null check(age > 0) +); diff --git a/examples/handle-404/api/404.sql b/examples/handle-404/api/404.sql new file mode 100644 index 00000000..e24e8a7e --- /dev/null +++ b/examples/handle-404/api/404.sql @@ -0,0 +1,9 @@ +SELECT 'debug' AS component, + 'api/404.sql' AS serving_file, + sqlpage.path() AS request_path; + +SELECT 'button' AS component; +SELECT + 'Back home' AS title, + 'home' AS icon, + '/' AS link; diff --git a/examples/handle-404/api/index.sql b/examples/handle-404/api/index.sql new file mode 100644 index 00000000..727beadd --- /dev/null +++ b/examples/handle-404/api/index.sql @@ -0,0 +1,9 @@ +SELECT + 'title' AS component, + 'Welcome to the API' AS contents; + +SELECT 'button' AS component; +SELECT + 'Back home' AS title, + 'home' AS icon, + '/' AS link; diff --git a/examples/handle-404/index.sql b/examples/handle-404/index.sql new file mode 100644 index 00000000..edb55e30 --- /dev/null +++ b/examples/handle-404/index.sql @@ -0,0 +1,42 @@ +SELECT 'list' AS component, + 'Navigation' AS title; + +SELECT + column1 AS title, column2 AS link, column3 AS description_md +FROM (VALUES + ('Link to arbitrary path', '/api/does/not/actually/exist', 'Covered by `api/404.sql`'), + ('Link to arbitrary file', '/api/nothing.png', 'Covered by `api/404.sql`'), + ('Link to non-existing .sql file', '/api/inexistent.sql', 'Covered by `api/404.sql`'), + ('Link to 404 handler', '/api/404.sql', 'Actually `api/404.sql`'), + ('Link to API landing page', '/api', 'Covered by `api/index.sql`'), + ('Link to arbitrary broken path', '/backend/does/not/actually/exist', 'Not covered by anything, will yield a 404 error') +); + +SELECT 'text' AS component, + ' +# Overview + +This demo shows how a `404.sql` file can serve as a fallback error handler. Whenever a `404 Not +Found` error would be emitted, instead a dedicated `404.sql` is called (if it exists) to serve the +request. This is usefull in two scenarios: + +1. Providing custom 404 error pages. +2. To provide content under dynamic paths. + +The former use-case is primarily of cosmetic nature, it allows for more informative, customized +failure modes, enabling better UX. The latter use-case opens the door especially for REST API +design, where dynamic paths are often used to convey arguments, i.e. `/api/resource/5` where `5` is +the id of a resource. + + +# Fallback Handler Selection + +When a normal request to either a `.sql` or a static file fails with `404`, the `404` error is +intercepted. The reuquest path''s target directory is scanned for a `404.sql`. If it exists, it is +called. Otherwise, the parent directory is scanned for `404.sql`, which will be called if it exists. +This search traverses up until it reaches the `web_root`. If even the webroot does not contain a +`404.sql`, then the original `404` error is served as response to the HTTP client. + +The fallback handler is not recursive; i.e. if anything causes another `404` during the call to a +`404.sql`, then the request fails (emitting a `404` response to the HTTP client). + ' AS contents_md; diff --git a/examples/image gallery with user uploads/sqlpage/sqlpage.json b/examples/image gallery with user uploads/sqlpage/sqlpage.json index 60322051..d7dc65b3 100644 --- a/examples/image gallery with user uploads/sqlpage/sqlpage.json +++ b/examples/image gallery with user uploads/sqlpage/sqlpage.json @@ -1,3 +1,3 @@ { - "max_uploaded_file_size": 5000000 -} \ No newline at end of file + "max_uploaded_file_size": 5000000 +} diff --git a/examples/light-dark-toggle/README.md b/examples/light-dark-toggle/README.md index c3c0838d..fb50c28b 100644 --- a/examples/light-dark-toggle/README.md +++ b/examples/light-dark-toggle/README.md @@ -1,6 +1,6 @@ # Switching between light mode and dark mode in SQLPage -This is a demo of a light/dark background toggle mecanism for websites built with [SQLpage](https://sql.ophir.dev/). +This is a demo of a light/dark background toggle mecanism for websites built with [SQLpage](https://sql-page.com/). ![screenshot](./screenshot.png) @@ -14,9 +14,9 @@ This example demonstrates: The SQL backend to this is SQLite, so the installation is easy: -1. [Install SQLpage](https://sql.ophir.dev/your-first-sql-website/) +1. [Install SQLpage](https://sql-page.com/your-first-sql-website/) -1. Clone SQLpage''s repository: `git clone https://github.com/lovasoa/SQLpage.git` +1. Clone SQLpage''s repository: `git clone https://github.com/sqlpage/SQLPage.git` 1. cd to `SQLpage/examples/light-dark-toggle` and run `sqlpage` in the cloned directory diff --git a/examples/light-dark-toggle/index.sql b/examples/light-dark-toggle/index.sql index 1da3aa1f..248f4862 100644 --- a/examples/light-dark-toggle/index.sql +++ b/examples/light-dark-toggle/index.sql @@ -7,15 +7,15 @@ SELECT 'title' AS component, TRUE AS center; SELECT 'text' AS component; -SELECT 'This is a demo of a light/dark background toggle mecanism for websites built with [SQLpage](https://sql.ophir.dev/), Ophir Lojkine''s fantastic tool +SELECT 'This is a demo of a light/dark background toggle mecanism for websites built with [SQLpage](https://sql-page.com/), Ophir Lojkine''s fantastic tool ## Installation The SQL backend to this is SQLite, so the installation is easy: -1. [Install SQLpage](https://sql.ophir.dev/your-first-sql-website/) +1. [Install SQLpage](https://sql-page.com/your-first-sql-website/) -1. Clone SQLpage''s repository: `git clone https://github.com/lovasoa/SQLpage.git` +1. Clone SQLpage''s repository: `git clone https://github.com/sqlpage/SQLPage.git` 1. cd to `SQLpage/examples/light-dark-toggle` and run `sqlpage` in the cloned directory diff --git a/examples/make a geographic data application using sqlite extensions/sqlpage/sqlpage.json b/examples/make a geographic data application using sqlite extensions/sqlpage/sqlpage.json index 3f615f2f..7835c9be 100644 --- a/examples/make a geographic data application using sqlite extensions/sqlpage/sqlpage.json +++ b/examples/make a geographic data application using sqlite extensions/sqlpage/sqlpage.json @@ -1,5 +1,3 @@ { - "sqlite_extensions": [ - "mod_spatialite" - ] -} \ No newline at end of file + "sqlite_extensions": ["mod_spatialite"] +} diff --git a/examples/master-detail-forms/README.md b/examples/master-detail-forms/README.md index c91eb2ac..a77181b1 100644 --- a/examples/master-detail-forms/README.md +++ b/examples/master-detail-forms/README.md @@ -18,7 +18,7 @@ and a second form to create their addresses. Once a user has been added, multiple addresses can be added to it. -See https://github.com/lovasoa/SQLpage/discussions/16 for more details. +See https://github.com/sqlpage/SQLPage/discussions/16 for more details. The main idea is to create two separate forms. In this example, we put both forms on the same page, in [`edit-user.sql`](./edit-user.sql). diff --git a/examples/microsoft sql server advanced forms/README.md b/examples/microsoft sql server advanced forms/README.md new file mode 100644 index 00000000..7a160280 --- /dev/null +++ b/examples/microsoft sql server advanced forms/README.md @@ -0,0 +1,34 @@ +# Handling json data in Microsoft SQL Server + +This demonstrates both how to produce and read json data from a SQL query +in MS SQL Server (or Azure SQL Database), for creating advanced forms. + +This lets your user interact with your database with a simple web interface, +even when you have multiple tables, with one-to-many relationships. + +![](./screenshots/app.png) + +## Documentation + +SQLPage requires JSON to create multi-select input (dropdowns where an user can select multiple values). +The result of these multi-selects is a JSON array, which also needs to be read by SQL queries. + +This example demonstrates how to consume [JSON](https://en.wikipedia.org/wiki/JSON) data from a SQL Server database, +using the [`OPENJSON`](https://docs.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql) +function to parse the JSON data into a table, +and [`FOR JSON PATH`](https://learn.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server) +to format query results as a JSON array. + + +This demonstrates an application designed for managing groups and users, allowing the creation of new groups, adding users, and assigning users to one or multiple groups. + +The application has the following sections: + +- **Create a New Group**: A form where users can enter the name of a new group. +- **Groups Display**: A list of existing groups. +- **Add a User**: A form where users can enter the name of a new user and select one or multiple groups to assign to this user. +- **Users Display**: A list of existing users and their associated group memberships. + +When users submit the form, their selections are packaged up and sent to the database server. The server receives these selections as a structured JSON array. + +The database then takes this list of selections and temporarily converts it into a format it can work with using the `OPENJSON` function, before saving the information permanently in the database tables. This allows the system to process multiple selections at once in an efficient way. diff --git a/examples/microsoft sql server advanced forms/docker-compose.yml b/examples/microsoft sql server advanced forms/docker-compose.yml new file mode 100644 index 00000000..416babde --- /dev/null +++ b/examples/microsoft sql server advanced forms/docker-compose.yml @@ -0,0 +1,32 @@ +services: + web: + image: lovasoa/sqlpage:main + ports: + - "8080:8080" + volumes: + - .:/var/www + - ./sqlpage:/etc/sqlpage + depends_on: + - db + environment: + RUST_LOG: sqlpage=debug + DATABASE_URL: mssql://sa:YourStrong!Passw0rd@db:1433/ + db: + ports: + - "1433:1433" + image: mcr.microsoft.com/mssql/server:2022-latest + volumes: + - ./sqlpage/mssql-migrations:/migrations + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: YourStrong!Passw0rd + MSSQL_PID: Express + command: > + bash -c " + /opt/mssql/bin/sqlservr & + until /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong!Passw0rd -C -Q 'SELECT 1;'; do + echo 'Waiting for database...' + sleep 1 + done + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong!Passw0rd -C -i /migrations/0001_db_init.sql + tail -f /dev/null" diff --git a/examples/microsoft sql server advanced forms/index.sql b/examples/microsoft sql server advanced forms/index.sql new file mode 100644 index 00000000..b8b1a21d --- /dev/null +++ b/examples/microsoft sql server advanced forms/index.sql @@ -0,0 +1,37 @@ +select 'form' as component, 'Create a new Group' as title, 'Create' as validate; +select 'Name' as name; + +insert into groups(name) select :Name where :Name is not null; + +select 'list' as component, 'Groups' as title, 'No group yet' as empty_title; +select name as title from groups; + +select 'form' as component, 'Add a user' as title, 'Add' as validate; +select 'UserName' as name, 'Name' as label; +select + 'Memberships[]' as name, + 'Group memberships' as label, + 'select' as type, + 1 as multiple, + 'press ctrl to select multiple values' as description, + ( + SELECT name as label, id as value + FROM groups + FOR JSON PATH -- this builds a JSON array of objects + ) as options; + +insert into users(name) select :UserName where :UserName is not null; + +insert into group_members(group_id, user_id) +select json_elem.value, IDENT_CURRENT('users') +from openjson(:Memberships) as json_elem +where :Memberships is not null; + +select 'list' as component, 'Users' as title, 'No user yet' as empty_title; +select + users.name as title, + string_agg(groups.name, ', ') as description +from users +left join group_members on users.id = group_members.user_id +left join groups on groups.id = group_members.group_id +group by users.id, users.name; \ No newline at end of file diff --git a/examples/microsoft sql server advanced forms/screenshots/app.png b/examples/microsoft sql server advanced forms/screenshots/app.png new file mode 100644 index 00000000..66077456 Binary files /dev/null and b/examples/microsoft sql server advanced forms/screenshots/app.png differ diff --git a/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/0001_db_init.sql b/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/0001_db_init.sql new file mode 100644 index 00000000..8748b56b --- /dev/null +++ b/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/0001_db_init.sql @@ -0,0 +1,35 @@ +create table users ( + id int primary key IDENTITY(1,1), + name varchar(255) not null +); + +create table groups ( + id int primary key IDENTITY(1,1), + name varchar(255) not null +); + +create table group_members ( + group_id int not null, + user_id int not null, + constraint PK_group_members primary key (group_id, user_id), + constraint FK_group_members_groups foreign key (group_id) references groups (id), + constraint FK_group_members_users foreign key (user_id) references users (id) +); + +CREATE TABLE questions( + id INT PRIMARY KEY IDENTITY(1,1), + question_text TEXT +); + +CREATE TABLE survey_answers( + id INT PRIMARY KEY IDENTITY(1,1), + question_id INT, + answer TEXT, + timestamp DATETIME DEFAULT GETDATE(), + CONSTRAINT FK_survey_answers_questions FOREIGN KEY (question_id) REFERENCES questions(id) +); + +INSERT INTO questions(question_text) VALUES + ('What is your name?'), + ('What is your age?'), + ('What is your favorite color?'); diff --git a/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/README.md b/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/README.md new file mode 100644 index 00000000..0e1d5b5b --- /dev/null +++ b/examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/README.md @@ -0,0 +1,11 @@ +# Migrations for Microsoft SQL Server + +This folder contains the migrations for the Microsoft SQL Server example. + +At the time of writing, SQLPage does not support applying migrations for Microsoft SQL Server +automatically, so we need to apply them manually. + +We write the migrations in a folder called `mssql-migrations`, instead of the usual `migrations` +folder, and we use the `sqlcmd` tool to apply them. + +See [how it is done in the docker-compose file](../../docker-compose.yml). diff --git a/examples/microsoft sql server advanced forms/survey.sql b/examples/microsoft sql server advanced forms/survey.sql new file mode 100644 index 00000000..e1406e10 --- /dev/null +++ b/examples/microsoft sql server advanced forms/survey.sql @@ -0,0 +1,27 @@ +SELECT 'form' as component, 'Survey' as title; +SELECT id as name, question_text as label, 'textarea' as type +FROM questions; + +-- Save all the answers to the database, whatever the number and id of the questions +INSERT INTO survey_answers (question_id, answer) +SELECT + question_id, + json_unquote( + json_extract( + sqlpage.variables('post'), + concat('$."', question_id, '"') + ) + ) +FROM json_table( + json_keys(sqlpage.variables('post')), + '$[*]' columns (question_id int path '$') +) as question_ids; + +-- Show the answers +select 'card' as component, 'Survey results' as title; +select + questions.question_text as title, + survey_answers.answer as description, + 'On ' || survey_answers.timestamp as footer +from survey_answers +inner join questions on questions.id = survey_answers.question_id; diff --git a/examples/modeling a many to many relationship with a form/sqlpage/migrations/03_sqlpage_shell.sql b/examples/modeling a many to many relationship with a form/sqlpage/migrations/03_sqlpage_shell.sql index f56bdf3e..46c16977 100644 --- a/examples/modeling a many to many relationship with a form/sqlpage/migrations/03_sqlpage_shell.sql +++ b/examples/modeling a many to many relationship with a form/sqlpage/migrations/03_sqlpage_shell.sql @@ -18,5 +18,5 @@ CREATE TABLE sqlpage_shell ( INSERT INTO sqlpage_shell ( component, title, link, menu_item, lang, description, font, font_size, icon, footer ) VALUES ( -'shell', 'SQL Blog', '/', 'topics', 'en-US', 'A cool SQL-only blog', 'Playfair Display', 21, 'book', 'This blog is written entirely in SQL with [SQLPage](https://sql.ophir.dev)' +'shell', 'SQL Blog', '/', 'topics', 'en-US', 'A cool SQL-only blog', 'Playfair Display', 21, 'book', 'This blog is written entirely in SQL with [SQLPage](https://sql-page.com)' ); \ No newline at end of file diff --git a/examples/modeling a many to many relationship with a form/write.sql b/examples/modeling a many to many relationship with a form/write.sql index 758285e1..8335bf69 100644 --- a/examples/modeling a many to many relationship with a form/write.sql +++ b/examples/modeling a many to many relationship with a form/write.sql @@ -6,6 +6,6 @@ SELECT 'Content' AS name, 'textarea' AS type, 'The content of the post. Write so SELECT 'Main Topic' AS name, 'select' AS type, 'The main topic of the post. This will be used to display the post in the main page.' AS description, - json_group_array(json_object("label", name, "value", id)) AS options + json_group_array(json_object('label', name, 'value', id)) AS options FROM topic; SELECT 'Topics[]' AS name, 'checkbox' AS type, 'Check if this post should also appear in the "' || topic.name || '" category.' AS description, topic.id AS value, topic.name AS label FROM topic; \ No newline at end of file diff --git a/examples/multiple-choice-question/README.md b/examples/multiple-choice-question/README.md index 92fb0c35..767a16c0 100644 --- a/examples/multiple-choice-question/README.md +++ b/examples/multiple-choice-question/README.md @@ -19,6 +19,6 @@ Just run the sqlpage binary (`./sqlpage.bin`) from this folder. ## Interesting files -[admin.sql](admin.sql) uses the [dynamic component](https://sql.ophir.dev/documentation.sql?component=dynamic#component) to create a single page with one form per MCQ option. +[admin.sql](admin.sql) uses the [dynamic component](https://sql-page.com/documentation.sql?component=dynamic#component) to create a single page with one form per MCQ option. -[website_header.json](website_header.json) contains the [shell](https://sql.ophir.dev/documentation.sql?component=shell#component) that is then used in all pages using the `dynamic` component to create a consistent look and feel between pages. \ No newline at end of file +[website_header.json](website_header.json) contains the [shell](https://sql-page.com/documentation.sql?component=shell#component) that is then used in all pages using the `dynamic` component to create a consistent look and feel between pages. \ No newline at end of file diff --git a/examples/multiple-choice-question/website_header.json b/examples/multiple-choice-question/website_header.json index 235afce0..95e4088d 100644 --- a/examples/multiple-choice-question/website_header.json +++ b/examples/multiple-choice-question/website_header.json @@ -1,11 +1,7 @@ { - "component": "shell", - "title": "SQLPage Questions", - "icon": "help-hexagon", - "link": "/index.sql", - "menu_item": [ - "index", - "results", - "admin" - ] -} \ No newline at end of file + "component": "shell", + "title": "SQLPage Questions", + "icon": "help-hexagon", + "link": "/index.sql", + "menu_item": ["index", "results", "admin"] +} diff --git a/examples/mysql json handling/docker-compose.yml b/examples/mysql json handling/docker-compose.yml index 0656b34e..dfa8e82b 100644 --- a/examples/mysql json handling/docker-compose.yml +++ b/examples/mysql json handling/docker-compose.yml @@ -1,6 +1,6 @@ services: web: - image: lovasoa/sqlpage:main # main is cutting edge, use lovasoa/sqlpage:latest for the latest stable version + image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version ports: - "8080:8080" volumes: diff --git a/examples/nginx/README.md b/examples/nginx/README.md new file mode 100644 index 00000000..46f38ee8 --- /dev/null +++ b/examples/nginx/README.md @@ -0,0 +1,169 @@ +# SQLPage with NGINX Example + +This example demonstrates how to set up SQLPage behind an NGINX reverse proxy using Docker Compose. It showcases various features such as rate limiting, URL rewriting, caching, and more. + +## Overview + +The setup consists of three main components: + +1. SQLPage: The main application server +2. NGINX: The reverse proxy +3. MySQL: The database + +## Getting Started + +1. Clone the repository and navigate to the `examples/nginx` directory. + +2. Start the services using Docker Compose: + + ```bash + docker compose up + ``` + +3. Access the application at `http://localhost`. + +## Docker Compose Configuration + +The `docker-compose.yml` file defines the services. + +### SQLPage Service + +The SQLPage service uses the latest SQLPage development image, sets up necessary volume mounts for configuration (on `/etc/sqlpage`) and website (on `/var/www`) files, and establishes a connection to the MySQL database. +It reads http requests from a Unix socket (instead of a TCP socket) for communication with NGINX. This removes the overhead of TCP/IP when nginx and sqlpage are running on the same machine. + +### NGINX Service + +The NGINX service uses the official Alpine-based image. It exposes port 80 and mounts the SQLPage socket and the [custom NGINX configuration file](nginx/nginx.conf). + +### MySQL Service + +This service sets up a MySQL database with predefined credentials and a persistent volume for data storage. + +## NGINX Configuration + +The `nginx.conf` file contains the NGINX configuration: + +### Streaming and compression + +SQLPage streams HTML as it is generated, so browsers can start rendering before the database finishes returning rows. NGINX enables `proxy_buffering` by default, which can delay those first bytes but stores responses for slow clients. Start with a modest buffer configuration and let the proxy handle compression: + +``` + proxy_buffering on; + proxy_buffer_size 16k; + proxy_buffers 4 16k; + + gzip on; + gzip_buffers 2 4k; + gzip_types text/html text/plain text/css application/javascript application/json; + + chunked_transfer_encoding on; +``` + +Keep buffering when you expect slow clients or longer SQLPage queries, increasing the buffer sizes only if responses overflow. When most users are on fast connections reading lightweight pages, consider reducing the buffer counts or flipping to `proxy_buffering off;` to minimise latency, accepting the extra load on SQLPage. See the [proxy buffering](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering), [gzip](https://nginx.org/en/docs/http/ngx_http_gzip_module.html), and [chunked transfer](https://nginx.org/en/docs/http/ngx_http_core_module.html#chunked_transfer_encoding) directives for more guidance. + +When SQLPage runs behind a reverse proxy, set `compress_responses` to `false` in its configuration (documented [here](https://github.com/sqlpage/SQLPage/blob/main/configuration.md)) so that NGINX can perform compression once at the edge. + +### Rate Limiting + + +```nginx + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; +``` + + +This line defines a rate limiting zone that allows 1 request per second per IP address. + +### Server Block + + +```nginx + server { + listen 80; + server_name localhost; + + location / { + limit_req zone=one burst=5; + + proxy_pass http://unix:/tmp/sqlpage/sqlpage.sock; + } + } +``` + + +The server block defines how NGINX handles incoming requests. + + +#### URL rewriting: + + +```nginx + rewrite ^/post/([0-9]+)$ /post.sql?id=$1 last; +``` + + +This line rewrites URLs like `/post/123` to `/post.sql?id=123`. + +#### Proxy configuration: + + +```nginx +proxy_pass http://unix:/tmp/sqlpage/sqlpage.sock; +``` + + + These lines configure NGINX to proxy requests to the SQLPage Unix socket. + +#### Caching: + + +```nginx + # Enable caching + proxy_cache_valid 200 60m; + proxy_cache_valid 404 10m; + proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; +``` + + + These lines enable caching of responses from SQLPage. + +#### Buffering: + + +```nginx + # Enable buffering + proxy_buffering on; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; +``` + + + These lines configure response buffering for improved performance. + +#### SQLPage Configuration + +The SQLPage configuration is stored in `sqlpage_config/sqlpage.json`: + + +```json +{ + "max_database_pool_connections": 10, + "database_connection_idle_timeout_seconds": 1800, + "max_uploaded_file_size": 10485760, + "compress_responses": false, + "environment": "production" +} +``` + + +This configuration sets various SQLPage options, including the maximum number of database connections and the environment. + +## Application Structure + +The application consists of several SQL files in the `website` directory: + +1. `index.sql`: Displays a list of blog posts +2. `post.sql`: Shows details of a specific post and its comments +3. `add_comment.sql`: Handles adding new comments + +The database schema and initial data are defined in [`sqlpage_config/migrations/000_init.sql`](sqlpage_config/migrations/000_init.sql). \ No newline at end of file diff --git a/examples/nginx/docker-compose.yml b/examples/nginx/docker-compose.yml new file mode 100644 index 00000000..ca7eb365 --- /dev/null +++ b/examples/nginx/docker-compose.yml @@ -0,0 +1,42 @@ +services: + sqlpage: + image: lovasoa/sqlpage:main + volumes: + - sqlpage_socket:/tmp/sqlpage + - ./sqlpage_config:/etc/sqlpage + - ./website:/var/www/ + environment: + - DATABASE_URL=mysql://sqlpage:sqlpage_password@mysql:3306/sqlpage_db + - SQLPAGE_UNIX_SOCKET=/tmp/sqlpage/sqlpage.sock + depends_on: + - mysql + + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - sqlpage_socket:/tmp/sqlpage:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./website:/var/www:ro + depends_on: + - sqlpage + command: > + sh -c " + adduser -D -u 1000 sqlpage || true && + nginx -g 'daemon off;' + " + + mysql: + image: mysql:8 + environment: + - MYSQL_ROOT_PASSWORD=root_password + - MYSQL_DATABASE=sqlpage_db + - MYSQL_USER=sqlpage + - MYSQL_PASSWORD=sqlpage_password + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: + sqlpage_socket: \ No newline at end of file diff --git a/examples/nginx/nginx/nginx.conf b/examples/nginx/nginx/nginx.conf new file mode 100644 index 00000000..7a13cca9 --- /dev/null +++ b/examples/nginx/nginx/nginx.conf @@ -0,0 +1,127 @@ +# Specify the user under which nginx will run. +# This enhances security by not running as root. +# In our case, we are using the sqlpage user (created in the docker-compose.yml file) +# so that the NGINX worker processes can access the SQLPage socket. +user sqlpage; + +# Set the number of worker processes. 'auto' detects the number of CPU cores. +# Alternative: Specific number like '4' for 4 worker processes. +worker_processes auto; + +# Define the file where error logs will be written. 'notice' sets the logging level. +# Alternative levels: debug, info, warn, error, crit, alert, emerg +error_log /var/log/nginx/error.log notice; + +# Specify the file where the main nginx process ID will be written +pid /var/run/nginx.pid; + +# Configuration for connection processing +events { + # Maximum number of simultaneous connections that can be opened by a worker process + # Can be increased for high traffic sites, but limited by system resources + worker_connections 1024; +} + +# Main HTTP server configuration block +# In a typical configuration, you would have one http block for all your applications +# and each application would be defined in a different file, in the /etc/nginx/sites-available/ directory +# and then enabled by creating a symlink to it in the /etc/nginx/sites-enabled/ directory. +http { + # This individual configuration files would start here, with only the contents + # from inside the http block. + + # Include MIME types definitions file + include /etc/nginx/mime.types; + + # Set the default MIME type if nginx can't determine it + default_type application/octet-stream; + + # Define the format of the access log entries + # This log format includes various details about each request + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # Specify the file where access logs will be written, using the 'main' format defined above + access_log /var/log/nginx/access.log main; + + # Enable the use of sendfile() for serving static files, which can improve performance + sendfile on; + + # Set the timeout for keep-alive connections with the client + # Can be adjusted based on your application's needs + keepalive_timeout 65; + + # Define a rate limiting zone to protect against DDoS attacks + # $binary_remote_addr uses less memory than $remote_addr + # 10m defines the memory size for storing IP addresses + # 1r/s sets the maximum rate of requests per second from a single IP + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; + + # Server block defining a virtual host + server { + # Listen on port 80 for HTTP connections + # If you want to listen on port 443 for HTTPS, you can use the certbot command to get a certificate + # and automatically configure NGINX to use it: + # sudo certbot --nginx -d yourdomain.com + listen 80; + + # Define the server name. 'localhost' is used here, but should be your domain in production + # server_name yourdomain.com; + server_name localhost; + + # Configuration for serving static files + # Note the trailing slash in the location block + # It is necessary because we want to serve files from /var/www/static/ + # and we want to allow users to request http://localhost/static/foo.js + # as well as http://localhost/static/dir/bar.js + location /static/ { + # Set the directory from which static files will be served + # This allows you to place static files in the `website/static/` directory + # and serve them at http://localhost:80/static/... + # This removes load from the SQLPage application that will only handle dynamic requests + alias /var/www/static/; + } + + # Configuration for proxying requests to SQLPage + location / { + # Apply rate limiting to this location + # burst=5 allows temporary bursts of requests + # This is useful to avoid DoS attacks + limit_req zone=one burst=5; + + # URL rewriting example for pretty URLs + # Rewrites /post/123 to /post.sql?id=123 + rewrite ^/post/([0-9]+)$ /post.sql?id=$1 last; + + # Proxy requests to a Unix socket where SQLPage is listening + proxy_pass http://unix:/tmp/sqlpage/sqlpage.sock; + + # Set headers for the proxied request + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Enable caching of proxied content + # Cache successful responses for 60 minutes and 404 responses for 10 minutes + proxy_cache_valid 200 60m; + proxy_cache_valid 404 10m; + + # Use stale cached content when upstream errors occur + proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; + + # Enable buffering of responses from the proxied server + proxy_buffering on; + + # Set the size of the buffer used for reading the first part of the response + proxy_buffer_size 128k; + + # Set the number and size of buffers used for reading a response + proxy_buffers 4 256k; + + # Limit the amount of data that can be stored in buffers while a response is being processed + proxy_busy_buffers_size 256k; + } + } +} \ No newline at end of file diff --git a/examples/nginx/sqlpage_config/migrations/000_init.sql b/examples/nginx/sqlpage_config/migrations/000_init.sql new file mode 100644 index 00000000..00968966 --- /dev/null +++ b/examples/nginx/sqlpage_config/migrations/000_init.sql @@ -0,0 +1,37 @@ +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT, + title VARCHAR(255) NOT NULL, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE comments ( + id INT AUTO_INCREMENT PRIMARY KEY, + post_id INT, + user_id INT, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (post_id) REFERENCES posts(id), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +INSERT INTO users (username, email) VALUES +('john_doe', 'john@example.com'), +('jane_smith', 'jane@example.com'); + +INSERT INTO posts (user_id, title, content) VALUES +(1, 'First Post', 'This is the content of the first post.'), +(2, 'Hello World', 'Hello everyone! This is my first post.'); + +INSERT INTO comments (post_id, user_id, content) VALUES +(1, 2, 'Great post!'), +(2, 1, 'Welcome to the community!'); diff --git a/examples/nginx/sqlpage_config/sqlpage.json b/examples/nginx/sqlpage_config/sqlpage.json new file mode 100644 index 00000000..1c564962 --- /dev/null +++ b/examples/nginx/sqlpage_config/sqlpage.json @@ -0,0 +1,7 @@ +{ + "max_database_pool_connections": 10, + "database_connection_idle_timeout_seconds": 1800, + "max_uploaded_file_size": 10485760, + "compress_responses": false, + "environment": "production" +} diff --git a/examples/nginx/website/add_comment.sql b/examples/nginx/website/add_comment.sql new file mode 100644 index 00000000..2d7db565 --- /dev/null +++ b/examples/nginx/website/add_comment.sql @@ -0,0 +1,2 @@ +INSERT INTO comments (post_id, user_id, content) VALUES ($id, 1, :content); +SELECT 'redirect' as component, '/post/' || $id AS link; \ No newline at end of file diff --git a/examples/nginx/website/index.sql b/examples/nginx/website/index.sql new file mode 100644 index 00000000..42711a57 --- /dev/null +++ b/examples/nginx/website/index.sql @@ -0,0 +1,10 @@ +SELECT 'list' AS component, 'Blog Posts' AS title; + +SELECT + p.title, + u.username AS description, + 'user' AS icon, + '/post/' || p.id AS link +FROM posts p +JOIN users u ON p.user_id = u.id +ORDER BY p.created_at DESC; \ No newline at end of file diff --git a/examples/nginx/website/post.sql b/examples/nginx/website/post.sql new file mode 100644 index 00000000..97e7dba2 --- /dev/null +++ b/examples/nginx/website/post.sql @@ -0,0 +1,46 @@ +-- Display the post content using the card component +SELECT 'card' as component, + 'Post Details' as title, + 1 as columns; +SELECT p.title as title, + u.username as subtitle, + p.content as description, + p.created_at as footer +FROM posts p +JOIN users u ON p.user_id = u.id +WHERE p.id = $id; + +-- Add a divider +SELECT 'divider' as component; + +-- Display comments using the list component +SELECT 'list' as component, + 'Comments' as title; +SELECT u.username as title, + c.content as description, + c.created_at as subtitle, + 'user' as icon, + CASE + WHEN c.user_id = p.user_id THEN 'blue' + ELSE 'gray' + END as color +FROM comments c +JOIN users u ON c.user_id = u.id +JOIN posts p ON c.post_id = p.id +WHERE c.post_id = $id +ORDER BY c.created_at DESC; + +-- Add a divider +SELECT 'divider' as component; + +-- Add a comment form +SELECT 'form' as component, + 'Add a comment' as title, + 'Post comment' as validate, + '/add_comment.sql?id=' || $id as action; + +SELECT 'textarea' as type, + 'content' as name, + 'Your comment' as label, + 'Write your comment here' as placeholder, + true as required; diff --git a/examples/official-site/404.sql b/examples/official-site/404.sql new file mode 100644 index 00000000..a788a905 --- /dev/null +++ b/examples/official-site/404.sql @@ -0,0 +1,8 @@ +select 'status_code' as component, 404 as status; +select 'http_header' as component, 'no-store, max-age=0' as "Cache-Control"; +select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; + +select 'hero' as component, + 'Not Found' as title, + 'Sorry, we couldn''t find the page you were looking for.' as description_md, + '/your-first-sql-website/not_found.jpg' as image; diff --git a/examples/official-site/README.md b/examples/official-site/README.md new file mode 100644 index 00000000..51255883 --- /dev/null +++ b/examples/official-site/README.md @@ -0,0 +1,12 @@ +# Official SQLPage site + +The SQLPage website is of course built with SQLPage ! + +Things that you may be interested to look at: + - The [custom component we use for our stylish home page](./sqlpage/templates/shell-home.handlebars) + - The [migrations](./sqlpage/migrations) that populate the database with the documentation for all components + - The [advanced multistep form example](./examples/multistep-form/) + +It is hosted as a simple Docker container on [sql-page.com](https://sql-page.com). + +Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything ! \ No newline at end of file diff --git a/examples/official-site/assets/db-bigquery.svg b/examples/official-site/assets/db-bigquery.svg new file mode 100644 index 00000000..21fbe250 --- /dev/null +++ b/examples/official-site/assets/db-bigquery.svg @@ -0,0 +1 @@ +Google BigQuery diff --git a/examples/official-site/assets/db-clickhouse.svg b/examples/official-site/assets/db-clickhouse.svg new file mode 100644 index 00000000..82d7d981 --- /dev/null +++ b/examples/official-site/assets/db-clickhouse.svg @@ -0,0 +1 @@ +ClickHouse diff --git a/examples/official-site/assets/db-databricks.svg b/examples/official-site/assets/db-databricks.svg new file mode 100644 index 00000000..129ea2ac --- /dev/null +++ b/examples/official-site/assets/db-databricks.svg @@ -0,0 +1 @@ +Databricks diff --git a/examples/official-site/assets/db-db2.svg b/examples/official-site/assets/db-db2.svg new file mode 100644 index 00000000..02811396 --- /dev/null +++ b/examples/official-site/assets/db-db2.svg @@ -0,0 +1 @@ +IBM \ No newline at end of file diff --git a/examples/official-site/assets/db-duckdb.svg b/examples/official-site/assets/db-duckdb.svg new file mode 100644 index 00000000..7e590871 --- /dev/null +++ b/examples/official-site/assets/db-duckdb.svg @@ -0,0 +1 @@ +DuckDB diff --git a/examples/official-site/assets/db-mysql.svg b/examples/official-site/assets/db-mysql.svg new file mode 100644 index 00000000..e1606ffd --- /dev/null +++ b/examples/official-site/assets/db-mysql.svg @@ -0,0 +1 @@ +MySQL diff --git a/examples/official-site/assets/db-odbc.svg b/examples/official-site/assets/db-odbc.svg new file mode 100644 index 00000000..b364b5a6 --- /dev/null +++ b/examples/official-site/assets/db-odbc.svg @@ -0,0 +1 @@ +ODBCODBC diff --git a/examples/official-site/assets/db-oracle.svg b/examples/official-site/assets/db-oracle.svg new file mode 100644 index 00000000..1e41072f --- /dev/null +++ b/examples/official-site/assets/db-oracle.svg @@ -0,0 +1 @@ +Oracle \ No newline at end of file diff --git a/examples/official-site/assets/db-postgres.svg b/examples/official-site/assets/db-postgres.svg new file mode 100644 index 00000000..d7ccd9e3 --- /dev/null +++ b/examples/official-site/assets/db-postgres.svg @@ -0,0 +1 @@ +PostgreSQL diff --git a/examples/official-site/assets/db-snowflake.svg b/examples/official-site/assets/db-snowflake.svg new file mode 100644 index 00000000..b62af544 --- /dev/null +++ b/examples/official-site/assets/db-snowflake.svg @@ -0,0 +1 @@ +Snowflake diff --git a/examples/official-site/assets/db-sqlite.svg b/examples/official-site/assets/db-sqlite.svg new file mode 100644 index 00000000..e6e77901 --- /dev/null +++ b/examples/official-site/assets/db-sqlite.svg @@ -0,0 +1 @@ +SQLite diff --git a/examples/official-site/assets/db-sqlserver.svg b/examples/official-site/assets/db-sqlserver.svg new file mode 100644 index 00000000..ecb4c222 --- /dev/null +++ b/examples/official-site/assets/db-sqlserver.svg @@ -0,0 +1 @@ +Microsoft SQL Server \ No newline at end of file diff --git a/examples/official-site/assets/highlightjs-and-tabler-theme.css b/examples/official-site/assets/highlightjs-and-tabler-theme.css new file mode 100644 index 00000000..a84cc4bd --- /dev/null +++ b/examples/official-site/assets/highlightjs-and-tabler-theme.css @@ -0,0 +1,204 @@ +@charset "utf-8"; + +:root, +.layout-boxed[data-bs-theme="dark"] { + color-scheme: dark; + + /* Core colors refined for better contrast */ + --tblr-body-color: hsl(225deg 35% 86%); + --tblr-secondary-color: hsl(225, 15%, 80%); + --tblr-muted-color: hsla(225, 15%, 75%, 0.8); + + /* Background system */ + --tblr-body-bg: hsl(225deg 44% 9%); + --tblr-bg-surface: hsl(225, 47%, 10%); + --tblr-bg-surface-secondary: hsl(225, 47%, 12%); + --tblr-bg-surface-tertiary: hsl(225, 47%, 14%); + + /* Border colors */ + --tblr-border-color: hsl(225deg, 26%, 19%); + --tblr-border-color-translucent: hsla(225deg 27% 19% / 0.7); + + /* Text secondary RGB */ + --tblr-text-secondary-rgb: + 204, 209, 217; /* RGB equivalent of hsl(225, 15%, 80%) */ + + /* Code colors */ + --tblr-code-color: hsl(225deg 45.4% 76.93%); /* Light code text for dark theme */ + --tblr-code-bg: hsla(225, 47%, 15%, 0.5); /* Subtle dark background */ + + /* Ethereal accent colors */ + --tblr-blue-rgb: 84, 151, 213; + --tblr-blue: rgb(var(--tblr-blue-rgb)); + --tblr-blue-lt-rgb: 21, 31, 53; + --tblr-blue-lt: rgb(var(--tblr-blue-lt-rgb)); + --tblr-primary-rgb: 95, 132, 169; + --tblr-primary: rgb(var(--tblr-primary-rgb)); + --tblr-secondary: hsla(247, 60%, 94%, 0.7); /* Nebula purple */ + + /* Luminous links */ + --tblr-link-color: hsl(212, 70%, 75%) !important; /* Star glow */ + --tblr-link-hover-color: hsl(212, 70%, 85%) !important; /* Supernova */ + --tblr-carousel-caption-color: var(--tblr-muted-color); + + /* Ethereal shadows */ + --tblr-box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.2), 0 0 15px rgba(66, 153, 225, 0.15); + --tblr-box-shadow-dropdown: + 0px 8px 24px rgba(0, 0, 0, 0.25), 1px 9px 20px rgba(174, 62, 201, 0.15); + + /* Pure white emphasis */ + --tblr-emphasis-color: #ffffff; + --tblr-heading-color: #ffffff; + + /* Syntax highlighting colors */ + --hljs-cosmic-comment: hsl(290, 10%, 60%); /* Distant star */ + --hljs-cosmic-punctuation: hsl(60, 40%, 85%); /* Stardust */ + --hljs-cosmic-property: hsl(340, 60%, 65%); /* Red giant */ + --hljs-cosmic-number: hsl(0, 50%, 75%); /* Solar flare */ + --hljs-cosmic-boolean: hsl(270, 100%, 75%); /* Purple nebula */ + --hljs-cosmic-string: hsl(120, 40%, 75%); /* Green aurora */ + --hljs-cosmic-operator: hsl(0, 0%, 95%); /* White dwarf */ + --hljs-cosmic-keyword: hsl(210, 100%, 75%); /* Blue giant */ +} + +@media (min-width: 768px) { + .layout-boxed { + background: #07020ff5 + linear-gradient(70deg, rgba(23, 17, 39, 0.4), transparent) fixed; + } +} + +/* Comments, Prolog, Doctype, and Cdata */ +.hljs-comment, +.hljs-prolog, +.hljs-meta, +.hljs-cdata { + color: var(--tblr-secondary-color); +} + +/* Punctuation */ +.hljs-template-variable, +.hljs-punctuation { + color: var(--hljs-cosmic-punctuation); +} + +/* Property and Tag */ +.hljs-property { + color: var(--hljs-cosmic-property); +} + +/* Number */ +.hljs-number { + color: var(--hljs-cosmic-number); +} + +/* Boolean */ +.hljs-literal { + color: var(--hljs-cosmic-boolean); +} + +/* String */ +.hljs-selector-tag, +.hljs-string { + color: var(--hljs-cosmic-string); +} + +/* Operator */ +.hljs-operator, +.hljs-symbol, +.hljs-link, +.language-css .hljs-string, +.style .hljs-string { + color: var(--hljs-cosmic-operator); +} + +/* Keyword */ +.hljs-template-tag, +.hljs-keyword { + color: var(--hljs-cosmic-keyword); +} + +/* Namespace */ +.hljs-namespace { + opacity: 0.7; +} + +/* Selector, Attr-name, and String */ +.hljs-attr { + color: #fcfce5; +} + +.hljs-name { + color: #e4faf6; +} + +/* Operator, Entity, URL, CSS String, and Style String */ +.hljs-operator, +.hljs-symbol, +.hljs-link, +.language-css .hljs-string, +.style .hljs-string { + color: #f8f8f2; +} + +/* At-rule and Attr-value */ +.hljs-tag, +.hljs-keyword, +.hljs-attribute-value { + color: #e6db74; +} + +/* Regex and Important */ +.hljs-regexp, +.hljs-important { + color: var(--tblr-yellow); +} + +/* Important */ +.hljs-important { + font-weight: bold; +} + +/* Entity */ +.hljs-symbol { + cursor: help; +} + +/* Token transition */ +.hljs { + transition: 0.3s; +} + +/* Code selection */ +code::selection, +code ::selection { + background: var(--tblr-yellow); + color: var(--tblr-gray-900); + border-radius: 0.1em; +} + +code .hljs-keyword::selection, +code .hljs-punctuation::selection { + color: var(--tblr-gray-800); +} + +/* Pre code padding */ +pre code { + padding: 0; +} + +/* Limit height and add inset shadow to code blocks */ +pre:has(code) { + max-height: 33vh; /* Limit height */ + overflow: auto; + box-shadow: inset 0 -1px 20px hsla(207.7, 39.4%, 6.5%, 0.64); + border-radius: 0.5rem; +} + +@media print { + pre:has(code) { + max-height: none !important; + box-shadow: none !important; + } +} diff --git a/examples/official-site/assets/highlightjs-launch.js b/examples/official-site/assets/highlightjs-launch.js new file mode 100644 index 00000000..66ce4773 --- /dev/null +++ b/examples/official-site/assets/highlightjs-launch.js @@ -0,0 +1 @@ +hljs.highlightAll(); diff --git a/examples/official-site/assets/icon.webp b/examples/official-site/assets/icon.webp new file mode 100644 index 00000000..b70cc059 Binary files /dev/null and b/examples/official-site/assets/icon.webp differ diff --git a/examples/official-site/assets/screenshots/big_tables.webm b/examples/official-site/assets/screenshots/big_tables.webm new file mode 100644 index 00000000..5a0a5f3c Binary files /dev/null and b/examples/official-site/assets/screenshots/big_tables.webm differ diff --git a/examples/official-site/assets/screenshots/plot.svg b/examples/official-site/assets/screenshots/plot.svg new file mode 100644 index 00000000..9c7ed226 --- /dev/null +++ b/examples/official-site/assets/screenshots/plot.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 120 + 100 + 80 + 60 + 40 + 20 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jan 25 + Apr 25 + Jul 25 + Oct 25 + 2026 + Apr 26 + + + select 'chart' as component;select date as x, revenue as yfrom sales;-- The end + diff --git a/examples/official-site/assets/screenshots/user-creation-form.png b/examples/official-site/assets/screenshots/user-creation-form.png new file mode 100644 index 00000000..93a30126 Binary files /dev/null and b/examples/official-site/assets/screenshots/user-creation-form.png differ diff --git a/examples/official-site/blog.sql b/examples/official-site/blog.sql index 68ef7931..4ed924e3 100644 --- a/examples/official-site/blog.sql +++ b/examples/official-site/blog.sql @@ -1,6 +1,13 @@ -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +select 'redirect' as component, '/blog.sql' as link +where ($post IS NULL AND sqlpage.path() <> '/blog.sql') OR ($post IS NOT NULL AND NOT EXISTS (SELECT 1 FROM blog_posts WHERE title = $post)); + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($post || ' - ', '') || 'SQLPage Blog' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; SELECT 'text' AS component, + true as article, content AS contents_md FROM blog_posts WHERE title = $post; diff --git a/examples/official-site/blog/pagination.png b/examples/official-site/blog/pagination.png new file mode 100644 index 00000000..4c4137ea Binary files /dev/null and b/examples/official-site/blog/pagination.png differ diff --git a/examples/official-site/colors.sql b/examples/official-site/colors.sql index d3c09ba0..480ac892 100644 --- a/examples/official-site/colors.sql +++ b/examples/official-site/colors.sql @@ -1,4 +1,11 @@ -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +set theme = coalesce($theme, 'custom'); + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', $theme || ' SQLPage Colors', + 'css', case $theme when 'custom' then '/assets/highlightjs-and-tabler-theme.css' end, + 'theme', case $theme when 'default' then 'light' else 'dark' end +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; create temporary table if not exists colors as select column1 as color, column2 as hex from (values ('blue', '#0054a6'), ('azure', '#4299e1'), ('indigo', '#4263eb'), ('purple', '#ae3ec9'), ('pink', '#d6336c'), ('red', '#d63939'), ('orange', '#f76707'), ('yellow', '#f59f00'), ('lime', '#74b816'), ('green', '#2fb344'), ('teal', '#0ca678'), ('cyan', '#17a2b8'), @@ -9,6 +16,77 @@ create temporary table if not exists colors as select column1 as color, column2 ('primary', '#0054a6'), ('secondary', '#49566c'), ('success', '#2fb344'), ('info', '#17a2b8'), ('warning', '#f59f00'), ('danger', '#d63939'), ('light', '#f1f5f9'), ('dark', '#0f172a') ); +select 'tab' as component; +select 'Default theme' as title, '?theme=default' as link, 'Default theme' as description, case $theme when 'default' then 'primary' end as color, $theme = 'default' as disabled; +select 'Custom theme' as title, '?theme=custom' as link, 'Custom theme' as description, case $theme when 'custom' then 'primary' end as color, $theme = 'custom' as disabled; + + select 'card' as component, 'Colors' as title; select color as title, hex as description, color as background_color -from colors; \ No newline at end of file +from colors; + + +select 'text' as component, ' +The colors above are from the [official site custom theme](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/assets/highlightjs-and-tabler-theme.css). +View [this page with the default theme](?theme=default) to see the colors that are used by default. +' as contents_md where $theme = 'custom'; + +select 'text' as component, ' +### Customization and theming + +SQLPage is designed to be easily customizable and themable. +You cannot pass arbitrary color codes to components from your SQL queries, +but you can customize which exact color is associated to each color name. + +#### Creating a custom theme + +To create a custom theme, you can create a CSS file and use the [shell component](/component.sql?component=shell) to include it. + +##### `index.sql` + +```sql +select ''shell'' as component, ''custom_theme.css'' as css, ''custom_theme'' as theme; +``` + +##### `custom_theme.css` + +```css +:root, +.layout-boxed[data-bs-theme="custom_theme"] { + color-scheme: light; + + /* Base text colors */ + --tblr-body-color: #cfd5e6; + --tblr-text-secondary-rgb: 204, 209, 217; + --tblr-secondary-color: #cccccc; + --tblr-muted-color: rgba(191, 191, 191, 0.8); + + /* Background colors */ + --tblr-body-bg: #0f1426; + --tblr-bg-surface: #111629; + --tblr-bg-surface-secondary: #151a2e; + --tblr-bg-surface-tertiary: #191f33; + + /* Primary and secondary colors */ + --tblr-primary-rgb: 95, 132, 169; + --tblr-primary: rgb(var(--tblr-primary-rgb)); + --tblr-secondary-rgb: 235, 232, 255; + --tblr-secondary: rgb(var(--tblr-secondary-rgb)); + + /* Border colors */ + --tblr-border-color: #151926; + --tblr-border-color-translucent: #404d73b3; + + /* Theme colors. All sqlpage colors can be customized in the same way. */ + --tblr-blue-rgb: 84, 151, 213; /* To convert between #RRGGBB color codes to decimal RGB values, you can use https://www.rapidtables.com/web/color/RGB_Color.html */ + --tblr-blue: rgb(var(--tblr-blue-rgb)); + + --tblr-red-rgb: 229, 62, 62; + --tblr-red: rgb(var(--tblr-red-rgb)); + + --tblr-green-rgb: 72, 187, 120; + --tblr-green: rgb(var(--tblr-green-rgb)); +} +``` +' as contents_md; + diff --git a/examples/official-site/component.sql b/examples/official-site/component.sql new file mode 100644 index 00000000..c10943f3 --- /dev/null +++ b/examples/official-site/component.sql @@ -0,0 +1,154 @@ +-- ensure that the component exists and do not render this page if it does not +select 'redirect' as component, + 'component_not_found.sql' || coalesce('?component=' || sqlpage.url_encode($component), '') as link +where not exists (select 1 from component where name = $component); + +-- This line, at the top of the page, tells web browsers to keep the page locally in cache once they have it. +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + printf('<%s>; rel="canonical"', sqlpage.link('component', json_object('component', $component))) as "Link"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($component || ' - ', '') || 'SQLPage Documentation' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +select 'breadcrumb' as component; +select 'SQLPage' as title, '/' as link, 'Home page' as description; +select 'Components' as title, '/documentation.sql' as link, 'List of all components' as description; +select $component as title, '/component.sql?component=' || sqlpage.url_encode($component) as link; + +select 'text' as component, 'component' as id, true as article, + format('# The **%s** component + +%s', $component, description) as contents_md +from component where name = $component; + +select format('Introduced in SQLPage v%s.', introduced_in_version) as contents, 1 as size +from component +where name = $component and introduced_in_version IS NOT NULL; + +select 'title' as component, 3 as level, 'Top-level parameters' as contents where $component IS NOT NULL; +select 'table' as component, true as striped, true as hoverable, true as freeze_columns, + 'type' as markdown + where $component IS NOT NULL; +select + name, + CASE WHEN optional THEN '' ELSE 'REQUIRED' END as required, + CASE type + WHEN 'COLOR' THEN printf('[%s](/colors.sql)', type) + WHEN 'ICON' THEN printf('[%s](https://tabler-icons.io/?ref=sqlpage)', type) + ELSE type + END AS type, + description +from parameter where component = $component AND top_level +ORDER BY optional, name; + + +select 'title' as component, 3 as level, 'Row-level parameters' as contents +WHERE $component IS NOT NULL AND EXISTS (SELECT 1 from parameter where component = $component AND NOT top_level); +select 'table' as component, true as striped, true as hoverable, true as freeze_columns, + 'type' as markdown + where $component IS NOT NULL; +select + name, + CASE WHEN optional THEN '' ELSE 'REQUIRED' END as required, + CASE type + WHEN 'COLOR' THEN printf('[%s](/colors.sql)', type) + WHEN 'ICON' THEN printf('[%s](https://tabler-icons.io/?ref=sqlpage)', type) + ELSE type + END AS type, + description +from parameter where component = $component AND NOT top_level +ORDER BY optional, name; + +select + 'dynamic' as component, + '[ + {"component": "code"}, + { + "title": "Example ' || (row_number() OVER ()) || '", + "description_md": ' || json_quote(description) || ', + "language": "sql", + "contents": ' || json_quote(( + select + group_concat( + 'select ' || char(10) || + ( + with t as ( + select *, + case type + when 'array' then json_array_length(value)>1 + else false + end as is_arr + from json_tree(top.value) + ), + key_val as (select + CASE t.type + WHEN 'integer' THEN t.atom + WHEN 'real' THEN t.atom + WHEN 'true' THEN 'TRUE' + WHEN 'false' THEN 'FALSE' + WHEN 'null' THEN 'NULL' + WHEN 'object' THEN 'JSON(' || quote(t.value) || ')' + WHEN 'array' THEN 'JSON(' || quote(t.value) || ')' + ELSE quote(t.value) + END as val, + CASE parent.fullkey + WHEN '$' THEN t.key + ELSE parent.key + END as key + from t inner join t parent on parent.id = t.parent + where ((parent.fullkey = '$' and not t.is_arr) + or (parent.path = '$' and parent.is_arr)) + ), + key_val_padding as (select + CASE + WHEN (key LIKE '% %') or (key LIKE '%-%') THEN + format('"%w"', key) + ELSE + key + END as key, + val, + 1 + max(0, max(case when length(val) < 30 then length(val) else 0 end) over () - length(val)) as padding + from key_val + ) + select group_concat( + format(' %s%.*cas %s', val, padding, ' ', key), + ',' || char(10) + ) from key_val_padding + ) || ';', + char(10) + ) + from json_each(properties) AS top + )) || ' + }, '|| + CASE component + WHEN 'shell' THEN '{"component": "text", "contents": ""}' + WHEN 'http_header' THEN '{ "component": "text", "contents": "" }' + ELSE ' + {"component": "title", "level": 3, "contents": "Result"}, + {"component": "dynamic", "properties": ' || properties ||' } + ' + END || ' + ] + ' as properties +from example where component = $component AND properties IS NOT NULL; + +SELECT 'title' AS component, 3 AS level, 'Examples' AS contents +WHERE EXISTS (SELECT 1 FROM example WHERE component = $component AND properties IS NULL); +SELECT 'text' AS component, description AS contents_md +FROM example WHERE component = $component AND properties IS NULL; + + +select 'title' as component, 2 as level, 'See also: other components' as contents; +select + 'button' as component, + 'sm' as size, + 'pill' as shape; +select + name as title, + icon, + sqlpage.set_variable('component', name) as link +from component +order by name; \ No newline at end of file diff --git a/examples/official-site/component_not_found.sql b/examples/official-site/component_not_found.sql index d52297b6..e4b9656b 100644 --- a/examples/official-site/component_not_found.sql +++ b/examples/official-site/component_not_found.sql @@ -1,3 +1,5 @@ +select 'http_header' as component, 'noindex' as "X-Robots-Tag"; + select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; select @@ -9,7 +11,7 @@ select 'Back to the documentation' as link_text; -- Friendly message after an XSS or SQL injection attempt -set $attack = CASE WHEN +set attack = CASE WHEN $component LIKE '%<%' or $component LIKE '%>%' or $component LIKE '%/%' or $component LIKE '%;%' or $component LIKE '%--%' or $component LIKE '%''%' or $component LIKE '%(%' THEN 'attacked' END; @@ -29,4 +31,4 @@ report it and we will fix it as soon as possible. ' as description where $attack = 'attacked'; select 'safety.sql' as link, 'More about SQLPage security' as title where $attack='attacked'; -select 'https://github.com/lovasoa/SQLpage/security' as link, 'Report a vulnerability' as title where $attack='attacked'; \ No newline at end of file +select 'https://github.com/sqlpage/SQLPage/security' as link, 'Report a vulnerability' as title where $attack='attacked'; \ No newline at end of file diff --git a/examples/official-site/custom_components.sql b/examples/official-site/custom_components.sql index 703b318f..ff2902a7 100644 --- a/examples/official-site/custom_components.sql +++ b/examples/official-site/custom_components.sql @@ -16,7 +16,7 @@ Each page in SQLPage is composed of a `shell` component, which contains the page title and the navigation bar, and a series of normal components that display the data. -The `shell` component is always present unless explicitly skipped via the `?_sqlpage_embed` query parameter. +The `shell` component is always present unless explicitly skipped via the `?_sqlpage_embed` query parameter. If you don''t call it explicitly, it will be invoked with the default parameters automatically before your first component invocation that tries to render data on the page. @@ -88,7 +88,7 @@ For instance, you can easily create a multi-column layout with the following cod ``` -For custom styling, you can write your own CSS files +For custom styling, you can write your own CSS files and include them in your page header. You can use the `css` parameter of the default [`shell`](./documentation.sql?component=shell#component) component, or create your own custom `shell` component with a `` tag. @@ -132,7 +132,7 @@ and SQLPage adds a few more: - `static_path`: returns the path to one of the static files bundled with SQLPage. Accepts arguments like `sqlpage.js`, `sqlpage.css`, `apexcharts.js`, etc. - `app_config`: returns the value of a configuration parameter from sqlpage''s configuration file, such as `max_uploaded_file_size`, `site_prefix`, etc. - `icon_img`: generate an svg icon from a *tabler* icon name -- `markdown`: renders markdown text +- `markdown`: renders markdown text. Accepts an optional 2nd argument `''allow_unsafe''` that will render embedded html blocks: use only on trusted content. See the [Commonmark spec](https://spec.commonmark.org/0.31.2/#html-blocks) for more info. - `each_row`: iterates over the rows of a query result - `typeof`: returns the type of a value (`string`, `number`, `boolean`, `object`, `array`, `null`) - `rfc2822_date`: formats a date as a string in the [RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.3) format, that is, `Thu, 21 Dec 2000 16:01:07 +0200` @@ -167,18 +167,18 @@ You can overwrite the default components, including the `shell` component, For example, if you want to change the appearance of the `shell` component, you can create a file called `sqlpage/templates/shell.handlebars` and write your own HTML in it. If you don''t want to start from scratch, you can copy the default `shell` component -[from the SQLPage source code](https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/shell.handlebars). +[from the SQLPage source code](https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/shell.handlebars). ## Examples All the default components are written in handlebars, and you can read their source code to learn how to write your own. -[See the default components source code](https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates). +[See the default components source code](https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates). Some interesting examples are: - - [The `shell` component](https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/shell.handlebars) - - [The `card` component](https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/card.handlebars): simple yet complete example of a component that displays a list of items. - - [The `table` component](https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/table.handlebars): more complex example of a component that uses + - [The `shell` component](https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/shell.handlebars) + - [The `card` component](https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/card.handlebars): simple yet complete example of a component that displays a list of items. + - [The `table` component](https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/table.handlebars): more complex example of a component that uses - the `eq`, `or`, and `sort` handlebars helpers, - the `../` syntax to access the parent context, - and the `@key` to work with objects whose keys are not known in advance. diff --git a/examples/official-site/documentation.sql b/examples/official-site/documentation.sql index bc0bd3e3..e848f351 100644 --- a/examples/official-site/documentation.sql +++ b/examples/official-site/documentation.sql @@ -1,7 +1,6 @@ -- ensure that the component exists and do not render this page if it does not -select 'redirect' as component, - 'component_not_found.sql?component=' || sqlpage.url_encode($component) as link -where $component is not null and not exists (select 1 from component where name = $component); +select 'redirect' as component, sqlpage.link('component.sql', json_object('component', $component)) as link +where $component is not null; -- This line, at the top of the page, tells web browsers to keep the page locally in cache once they have it. select 'http_header' as component, 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; @@ -13,11 +12,11 @@ FROM example WHERE component = 'shell' LIMIT 1; select 'text' as component, format('SQLPage v%s documentation', sqlpage.version()) as title; select ' -If you are completely new to SQLPage, you should start by reading the [get started tutorial](get%20started.sql), +If you are completely new to SQLPage, you should start by reading the [get started tutorial](/your-first-sql-website/), which will guide you through the process of creating your first SQLPage application. Building an application with SQLPage is quite simple. -To create a new web page, just create a new SQL file. +To create a new web page, just create a new SQL file. For each SELECT statement that you write, the data it returns will be analyzed and rendered to the user. The two most important concepts in SQLPage are **components** and **parameters**. @@ -25,7 +24,7 @@ The two most important concepts in SQLPage are **components** and **parameters** - *top-level* **parameters** are the properties of these components, allowing you to customize their appearance and behavior. - *row-level* **parameters** constitute the data that you want to display in the components. -To select a component and set its top-level properties, you write the following SQL statement: +To select a component and set its top-level properties, you write the following SQL statement: ```sql SELECT ''component_name'' AS component, ''my value'' AS top_level_parameter_1; @@ -39,7 +38,10 @@ SELECT my_column_1 AS row_level_parameter_1, my_column_2 AS row_level_parameter_ This page documents all the components provided by default in SQLPage and their parameters. Use this as a reference when building your SQL application. -If at any point you need help, you can ask for it on the [SQLPage forum](https://github.com/lovasoa/SQLpage/discussions). +For more information about SQLPage variables and [SQLPage functions](/functions), +read about [the SQLPage data model](/extensions-to-sql). + +If at any point you need help, you can ask for it on the [SQLPage forum](https://github.com/sqlpage/SQLPage/discussions). If you know some [HTML](https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics), you can also easily [create your own components for your application](./custom_components.sql). @@ -50,120 +52,6 @@ select name as title, description, icon, - '?component='||name||'#component' as link, - $component = name as active + sqlpage.link('component.sql', json_object('component', name)) as link from component order by name; - -select 'text' as component, - 'The "'||$component||'" component' as title, - 'component' as id; -select description as contents from component where name = $component; - -select 'text' as component; -select format('Introduced in SQLPage v%s.', introduced_in_version) as contents, 1 as size -from component -where name = $component and introduced_in_version IS NOT NULL; - -select 'title' as component, 3 as level, 'Top-level parameters' as contents where $component IS NOT NULL; -select 'card' as component, 3 AS columns where $component IS NOT NULL; -select - name as title, - (CASE WHEN optional THEN '' ELSE 'REQUIRED. ' END) || description as description, - (CASE WHEN optional THEN '' ELSE 'REQUIRED. ' END) || description_md as description_md, - type as footer, - CASE type - WHEN 'COLOR' THEN '/colors.sql' - WHEN 'ICON' THEN 'https://tabler-icons.io/' - END AS footer_link, - CASE WHEN optional THEN 'lime' ELSE 'azure' END as color -from parameter where component = $component AND top_level -ORDER BY optional, name; - - -select 'title' as component, 3 as level, 'Row-level parameters' as contents -WHERE $component IS NOT NULL AND EXISTS (SELECT 1 from parameter where component = $component AND NOT top_level); -select 'card' as component, 3 AS columns where $component IS NOT NULL; -select - name as title, - (CASE WHEN optional THEN '' ELSE 'REQUIRED. ' END) || description as description, - (CASE WHEN optional THEN '' ELSE 'REQUIRED. ' END) || description_md as description_md, - type as footer, - CASE type - WHEN 'COLOR' THEN '/colors.sql' - WHEN 'ICON' THEN 'https://tabler-icons.io/' - END AS footer_link, - CASE WHEN optional THEN 'lime' ELSE 'azure' END as color -from parameter where component = $component AND NOT top_level -ORDER BY optional, name; - -select - 'dynamic' as component, - '[ - {"component": "code"}, - { - "title": "Example ' || (row_number() OVER ()) || '", - "description_md": ' || json_quote(description) || ', - "language": "sql", - "contents": ' || json_quote(( - select - group_concat( - 'select ' || char(10) || - ( - with t as (select * from json_tree(top.value)), - key_val as (select - CASE t.type - WHEN 'integer' THEN t.atom - WHEN 'real' THEN t.atom - WHEN 'true' THEN 'TRUE' - WHEN 'false' THEN 'FALSE' - WHEN 'null' THEN 'NULL' - WHEN 'object' THEN 'JSON(' || quote(t.value) || ')' - WHEN 'array' THEN 'JSON(' || quote(t.value) || ')' - ELSE quote(t.value) - END as val, - CASE parent.fullkey - WHEN '$' THEN t.key - ELSE parent.key - END as key - from t inner join t parent on parent.id = t.parent - where ((parent.fullkey = '$' and t.type != 'array') - or (parent.type = 'array' and parent.path = '$')) - ), - key_val_padding as (select - CASE - WHEN (key LIKE '% %') or (key LIKE '%-%') THEN - format('"%w"', key) - ELSE - key - END as key, - val, - 1 + max(0, max(case when length(val) < 30 then length(val) else 0 end) over () - length(val)) as padding - from key_val - ) - select group_concat( - format(' %s%.*cas %s', val, padding, ' ', key), - ',' || char(10) - ) from key_val_padding - ) || ';', - char(10) - ) - from json_each(properties) AS top - )) || ' - }, '|| - CASE component - WHEN 'shell' THEN '{"component": "text", "contents": ""}' - WHEN 'http_header' THEN '{ "component": "text", "contents": "" }' - ELSE ' - {"component": "title", "level": 3, "contents": "Result"}, - {"component": "dynamic", "properties": ' || properties ||' } - ' - END || ' - ] - ' as properties -from example where component = $component AND properties IS NOT NULL; - -SELECT 'title' AS component, 3 AS level, 'Examples' AS contents -WHERE EXISTS (SELECT 1 FROM example WHERE component = $component AND properties IS NULL); -SELECT 'text' AS component, description AS contents_md -FROM example WHERE component = $component AND properties IS NULL; diff --git a/examples/official-site/examples/authentication/basic_auth.sql b/examples/official-site/examples/authentication/basic_auth.sql index f6c65dc2..5eed4078 100644 --- a/examples/official-site/examples/authentication/basic_auth.sql +++ b/examples/official-site/examples/authentication/basic_auth.sql @@ -1,3 +1,5 @@ +select 'http_header' as component, 'noindex' as "X-Robots-Tag"; + SELECT 'authentication' AS component, case sqlpage.basic_auth_username() when 'admin' @@ -12,7 +14,7 @@ select 'dynamic' as component, properties FROM example WHERE component = 'shell' select 'text' as component, ' # Authentication -Read the [source code](//github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/authentication/basic_auth.sql) for this demo. +Read the [source code](//github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/authentication/basic_auth.sql) for this demo. ' as contents_md; select 'alert' as component, 'info' as color, CONCAT('You are logged in as ', sqlpage.basic_auth_username()) as title; diff --git a/examples/official-site/examples/authentication/create_session_token.sql b/examples/official-site/examples/authentication/create_session_token.sql index 645fedc4..8ea8cd19 100644 --- a/examples/official-site/examples/authentication/create_session_token.sql +++ b/examples/official-site/examples/authentication/create_session_token.sql @@ -4,12 +4,12 @@ delete from user_sessions where created_at < datetime('now', '-1 day'); -- check that the SELECT 'authentication' AS component, 'login.sql?failed' AS link, -- redirect to the login page on error - (SELECT password_hash FROM users WHERE username = :Username) AS password_hash, -- this is a hash of the password 'admin' - :Password AS password; -- this is the password that the user sent through our form in 'index.sql' + (SELECT password_hash FROM users WHERE username = :username) AS password_hash, -- this is a hash of the password 'admin' + :password AS password; -- this is the password that the user sent through our form in 'index.sql' -- if we haven't been redirected, then the password is correct -- create a new session -insert into user_sessions (session_token, username) values (sqlpage.random_string(32), :Username) +insert into user_sessions (session_token, username) values (sqlpage.random_string(32), :username) returning 'cookie' as component, 'session_token' as name, session_token as value; -- redirect to the authentication example home page diff --git a/examples/official-site/examples/authentication/index.sql b/examples/official-site/examples/authentication/index.sql index ce323484..32d2b6dc 100644 --- a/examples/official-site/examples/authentication/index.sql +++ b/examples/official-site/examples/authentication/index.sql @@ -1,18 +1,18 @@ -- redirect the user to the login page if they are not logged in -- this query should be present at the top of every page that requires authentication -set $role = (select role from users natural join user_sessions where session_token = sqlpage.cookie('session_token')); -select 'redirect' as component, 'login.sql' as link where $role is null; +set user_role = (select role from users natural join user_sessions where session_token = sqlpage.cookie('session_token')); +select 'redirect' as component, 'login.sql' as link where $user_role is null; select 'dynamic' as component, json_insert(properties, '$[0].menu_item[#]', 'logout') as properties FROM example WHERE component = 'shell' LIMIT 1; -select 'alert' as component, 'info' as color, CONCAT('You are logged in as ', $role) as title; +select 'alert' as component, 'info' as color, CONCAT('You are logged in as ', $user_role) as title; select 'text' as component, ' # Authentication -Read the [source code](//github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/authentication/) for this demo. +Read the [source code](//github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/authentication/) for this demo. [Log out](logout.sql) ' as contents_md; \ No newline at end of file diff --git a/examples/official-site/examples/authentication/login.sql b/examples/official-site/examples/authentication/login.sql index b634de30..b0a39f5b 100644 --- a/examples/official-site/examples/authentication/login.sql +++ b/examples/official-site/examples/authentication/login.sql @@ -1,10 +1,15 @@ select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; -select 'form' as component, 'Authentication' as title, 'Log in' as validate, 'create_session_token.sql' as action; -select 'Username' as name, 'user' as prefix_icon, 'admin' as placeholder; -select 'Password' as name, 'lock' as prefix_icon, 'admin' as placeholder, 'password' as type; - -select 'alert' as component, 'danger' as color, 'Invalid username or password' as title where $failed is not null; +select + 'login' as component, + 'create_session_token.sql' as action, + '/assets/icon.webp' as image, + 'Demo Login Form' as title, + 'Username' as username, + 'Password' as password, + case when $failed is not null then 'Invalid username or password. In this demo, you can log in with admin / admin.' end as error_message, + 'In this demo, the username is "admin" and the password is "admin".' as footer_md, + 'Log in' as validate; select 'text' as component, ' @@ -12,7 +17,7 @@ select 'text' as component, ' This is a simple example of an authentication form. It uses - - the [`form`](/documentation.sql?component=form#component) component to create a login form + - the [`login`](/documentation.sql?component=login#component) component to create a login form - the [`authentication`](/documentation.sql?component=authentication#component) component to check the user password - the [`cookie`](/documentation.sql?component=cookie#component) component to store a unique session token in the user browser - the [`redirect`](/documentation.sql?component=redirect#component) component to redirect the user to the login page if they are not logged in diff --git a/examples/official-site/examples/chart.sql b/examples/official-site/examples/chart.sql index 30563ad0..3fb18afb 100644 --- a/examples/official-site/examples/chart.sql +++ b/examples/official-site/examples/chart.sql @@ -5,7 +5,8 @@ select 'Syracuse Sequence' as title, coalesce($type, 'area') as type, coalesce($color, 'indigo') as color, - 5 as marker; + 5 as marker, + 0 as ymin; with recursive seq(x, y) as ( select 0, CAST($n as integer) union all diff --git a/examples/official-site/examples/csv_download.sql b/examples/official-site/examples/csv_download.sql new file mode 100644 index 00000000..14dbd9bc --- /dev/null +++ b/examples/official-site/examples/csv_download.sql @@ -0,0 +1,2 @@ +select 'csv' as component, 'example.csv' as filename; +select * from component; diff --git a/examples/official-site/examples/dynamic_shell.sql b/examples/official-site/examples/dynamic_shell.sql index 0669b168..9a76a338 100644 --- a/examples/official-site/examples/dynamic_shell.sql +++ b/examples/official-site/examples/dynamic_shell.sql @@ -15,15 +15,15 @@ SELECT 'dynamic' AS component, json_object( 'title', 'Blog' ), json_object( - 'link', 'https://github.com/lovasoa/sqlpage/issues', + 'link', 'https://github.com/sqlpage/SQLPage/issues', 'title', 'Issues' ), json_object( - 'link', 'https://github.com/lovasoa/sqlpage/discussions', + 'link', 'https://github.com/sqlpage/SQLPage/discussions', 'title', 'Discussions' ), json_object( - 'link', 'https://github.com/lovasoa/sqlpage', + 'link', 'https://github.com/sqlpage/SQLPage', 'title', 'Github' ) ) @@ -59,15 +59,15 @@ SELECT ''dynamic'' AS component, json_object( ''title'', ''Blog'' ), json_object( - ''link'', ''https://github.com/lovasoa/sqlpage/issues'', + ''link'', ''https://github.com/sqlpage/SQLPage/issues'', ''title'', ''Issues'' ), json_object( - ''link'', ''https://github.com/lovasoa/sqlpage/discussions'', + ''link'', ''https://github.com/sqlpage/SQLPage/discussions'', ''title'', ''Discussions'' ), json_object( - ''link'', ''https://github.com/lovasoa/sqlpage'', + ''link'', ''https://github.com/sqlpage/SQLPage'', ''title'', ''Github'' ) ) @@ -93,9 +93,9 @@ INSERT INTO menu_items (id, title, link, parent_id) VALUES (1, ''Home'', ''/'', NULL), (2, ''Community'', NULL, NULL), (3, ''Blog'', ''blog.sql'', 2), - (4, ''Issues'', ''https://github.com/lovasoa/sqlpage/issues'', 2), - (5, ''Discussions'', ''https://github.com/lovasoa/sqlpage/discussions'', 2), - (6, ''Github'', ''https://github.com/lovasoa/sqlpage'', 2); + (4, ''Issues'', ''https://github.com/sqlpage/SQLPage/issues'', 2), + (5, ''Discussions'', ''https://github.com/sqlpage/SQLPage/discussions'', 2), + (6, ''Github'', ''https://github.com/sqlpage/SQLPage'', 2); ``` Then, one could use the following SQL query to fetch diff --git a/examples/official-site/examples/form.sql b/examples/official-site/examples/form.sql new file mode 100644 index 00000000..913074e5 --- /dev/null +++ b/examples/official-site/examples/form.sql @@ -0,0 +1,92 @@ +select 'shell' as component, 'dark' as theme, '[View source on Github](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/form.sql)' as footer; + +SELECT 'form' AS component, 'Complete Input Types Reference' AS title, '/examples/show_variables.sql' as action; + +SELECT 'header' AS type, 'Text Input Types' AS label; + +SELECT 'username' AS name, 'text' AS type, 'Enter your username' AS placeholder, + '**Text** - Default single-line text input. Use for short text like names, usernames, titles. Supports `minlength`, `maxlength`, `pattern` for validation.' AS description_md; + +SELECT 'password' AS name, 'password' AS type, '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$' AS pattern, + '**Password** - Masked text input that hides characters. Use for passwords and sensitive data. Combine with `pattern` attribute for password strength requirements.' AS description_md; + +SELECT 'search_query' AS name, 'search' AS type, 'Search...' AS placeholder, + '**Search** - Search input field, may display a clear button. Use for search boxes. Mobile browsers may show optimized keyboard.' AS description_md; + +SELECT 'bio' AS name, 'textarea' AS type, 5 AS rows, 'Tell us about yourself...' AS placeholder, + '**Textarea** (SQLPage custom) - Multi-line text input. Use for long text like comments, descriptions, articles. Set `rows` to control initial height.' AS description_md; + +SELECT 'header' AS type, 'Numeric Input Types' AS label; + +SELECT 'age' AS name, 'number' AS type, 0 AS min, 120 AS max, 1 AS step, + '**Number** - Numeric input with up/down arrows. Use for quantities, ages, counts. Supports `min`, `max`, `step`. Mobile shows numeric keyboard.' AS description_md; + +SELECT 'price' AS name, 'number' AS type, 0.01 AS step, '$' AS prefix, + '**Number with decimals** - Set `step="0.01"` for currency. Use `prefix`/`suffix` for units. Great for prices, measurements, percentages.' AS description_md; + +SELECT 'volume' AS name, 'range' AS type, 0 AS min, 100 AS max, 50 AS value, 1 AS step, + '**Range** - Slider control for selecting a value. Use for volume, brightness, ratings, or any bounded numeric value where precision isn''t critical.' AS description_md; + +SELECT 'header' AS type, 'Date and Time Types' AS label; + +SELECT 'birth_date' AS name, 'date' AS type, + '**Date** - Date picker (year, month, day). Use for birthdays, deadlines, event dates. Most browsers show a calendar widget. Supports `min` and `max` for date ranges.' AS description_md; + +SELECT 'appointment_time' AS name, 'time' AS type, + '**Time** - Time picker (hours and minutes). Use for appointment times, opening hours, alarms. Shows time selector in supported browsers.' AS description_md; + +SELECT 'meeting_datetime' AS name, 'datetime-local' AS type, + '**Datetime-local** - Date and time picker without timezone. Use for scheduling events, booking appointments, logging timestamps in local time.' AS description_md; + +SELECT 'birth_month' AS name, 'month' AS type, + '**Month** - Month and year picker. Use for credit card expiration dates, monthly reports, subscription periods.' AS description_md; + +SELECT 'vacation_week' AS name, 'week' AS type, + '**Week** - Week and year picker. Use for week-based scheduling, timesheet entry, weekly reports.' AS description_md; + +SELECT 'header' AS type, 'Contact Information Types' AS label; + +SELECT 'user_email' AS name, 'email' AS type, 'user@example.com' AS placeholder, + '**Email** - Email address input with built-in validation. Use for email fields. Browser validates format automatically. Mobile shows @ key on keyboard.' AS description_md; + +SELECT 'phone' AS name, 'tel' AS type, '+1 (555) 123-4567' AS placeholder, + '**Tel** - Telephone number input. Use for phone numbers. Mobile browsers show numeric keyboard with phone symbols. No automatic validation - use `pattern` if needed.' AS description_md; + +SELECT 'website' AS name, 'url' AS type, 'https://example.com' AS placeholder, + '**URL** - URL input with validation. Use for website addresses, links. Browser validates URL format. Mobile may show .com key on keyboard.' AS description_md; + +SELECT 'header' AS type, 'Selection Types' AS label; + +SELECT 'country' AS name, 'select' AS type, + '[{"label": "United States", "value": "US"}, {"label": "Canada", "value": "CA"}, {"label": "United Kingdom", "value": "GB"}]' AS options, + '**Select** (SQLPage custom) - Dropdown menu. Use for single choice from many options. Add `multiple` for multi-select. Use `searchable` for long lists. Set `dropdown` for enhanced UI.' AS description_md; + +SELECT 'gender' AS name, 'radio' AS type, 'Male' AS value, 'Male' AS label, + '**Radio** - Radio button for mutually exclusive choices. Create multiple rows with same `name` for a radio group. One option can be selected. Use for 2-5 options.' AS description_md; + +SELECT 'gender' AS name, 'radio' AS type, 'Female' AS value, 'Female' AS label; + +SELECT 'gender' AS name, 'radio' AS type, 'Other' AS value, 'Other' AS label; + +SELECT 'interests' AS name, 'checkbox' AS type, 'Technology' AS value, 'Technology' AS label, + '**Checkbox** - Checkbox for multiple selections. Each checkbox is independent. Use for yes/no questions or multiple selections from a list.' AS description_md; + +SELECT 'terms' AS name, 'checkbox' AS type, TRUE AS required, 'I accept the terms and conditions' AS label, + '**Checkbox (required)** - Use `required` to make acceptance mandatory. Common for terms of service, privacy policies, consent forms.' AS description_md; + +SELECT 'notifications' AS name, 'switch' AS type, 'Enable email notifications' AS label, TRUE AS checked, + '**Switch** (SQLPage custom) - Toggle switch, styled checkbox alternative. Use for on/off settings, feature toggles, preferences. More intuitive than checkboxes for boolean settings.' AS description_md; + +SELECT 'header' AS type, 'File and Media Types' AS label; + +SELECT 'profile_picture' AS name, 'file' AS type, 'image/*' AS accept, + '**File** - File upload control. Use `accept` to limit file types (image/\*, .pdf, .doc). Use `multiple` to allow multiple files. Automatically sets form enctype to multipart/form-data.' AS description_md; + +SELECT 'documents[]' AS name, 'Documents' as label, 'file' AS type, '.pdf,.doc,.docx' AS accept, TRUE AS multiple, + '**File (multiple)** - Allow multiple file uploads with `multiple` attribute. Specify exact extensions or MIME types in `accept`.' AS description_md; + +SELECT 'favorite_color' AS name, 'color' AS type, '#3b82f6' AS value, + '**Color** - Color picker. Use for theme customization, design settings, highlighting preferences. Returns hex color code (#RRGGBB).' AS description_md; + +SELECT 'user_id' AS name, 'hidden' AS type, '12345' AS value, + '**Hidden** - Hidden input, not visible to users. Use for IDs, tokens, state information that needs to be submitted but not displayed or edited.' AS description_md; diff --git a/examples/official-site/examples/from_component_options_source.sql b/examples/official-site/examples/from_component_options_source.sql new file mode 100644 index 00000000..b7be6893 --- /dev/null +++ b/examples/official-site/examples/from_component_options_source.sql @@ -0,0 +1,5 @@ +select 'json' as component; + +select name as value, name as label +from component +where name like '%' || $search || '%'; \ No newline at end of file diff --git a/examples/official-site/examples/handle_picture_upload.sql b/examples/official-site/examples/handle_picture_upload.sql index 66700335..2b2e7b3f 100644 --- a/examples/official-site/examples/handle_picture_upload.sql +++ b/examples/official-site/examples/handle_picture_upload.sql @@ -2,7 +2,7 @@ select 'dynamic' as component, properties FROM example WHERE component = 'shell' select 'title' as component, 'SQLPage Image Upload Demo' as contents; -set $data_url = sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path('my_file')); +set data_url = sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path('my_file')); select 'card' as component, 1 as columns where $data_url is not null; select 'Your picture' as title, @@ -32,5 +32,5 @@ select ''card'' as component, 1 as columns; select ''Your picture'' as title, sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path(''my_file'')) as top_image; ``` -[See the source code of this page](https://github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/handle_picture_upload.sql). +[See the source code of this page](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/handle_picture_upload.sql). ' as contents_md; \ No newline at end of file diff --git a/examples/official-site/examples/hash_password.sql b/examples/official-site/examples/hash_password.sql index f6ccb882..a9e85049 100644 --- a/examples/official-site/examples/hash_password.sql +++ b/examples/official-site/examples/hash_password.sql @@ -29,8 +29,8 @@ you can store their session identifier on their browser using the ## Example - - [Source code for this page](https://github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/hash_password.sql) - - [Full user authentication and session management example](https://github.com/lovasoa/SQLpage/blob/main/examples/user-authentication) + - [Source code for this page](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/hash_password.sql) + - [Full user authentication and session management example](https://github.com/sqlpage/SQLPage/blob/main/examples/user-authentication) # Try it out diff --git a/examples/official-site/examples/index.sql b/examples/official-site/examples/index.sql new file mode 100644 index 00000000..371d206c --- /dev/null +++ b/examples/official-site/examples/index.sql @@ -0,0 +1 @@ +select 'redirect' as component, '/examples/tabs' as link; \ No newline at end of file diff --git a/examples/official-site/examples/layouts.sql b/examples/official-site/examples/layouts.sql index 234f4698..1169d771 100644 --- a/examples/official-site/examples/layouts.sql +++ b/examples/official-site/examples/layouts.sql @@ -1,3 +1,5 @@ +select 'http_header' as component, 'nofollow' as "X-Robots-Tag"; -- nofollow to avoid duplicate content + set layout = coalesce($layout, 'boxed'); set sidebar = coalesce($sidebar, 0); @@ -28,7 +30,7 @@ For more information on how to use layouts, see the [shell component documentati select 'list' as component, 'Available SQLPage shell layouts' as title; select column1 as title, - sqlpage.link('', json_object('layout', lower(column1), 'sidebar', $sidebar)) as link, + sqlpage.set_variable('layout', lower(column1)) as link, $layout = lower(column1) as active, column3 as icon, column2 as description @@ -41,7 +43,7 @@ from (VALUES select 'list' as component, 'Available Menu layouts' as title; select column1 as title, - sqlpage.link('', json_object('layout', $layout, 'sidebar', column1 = 'Sidebar')) as link, + sqlpage.set_variable('sidebar', column1 = 'Sidebar') as link, (column1 = 'Sidebar' AND $sidebar = 1) OR (column1 = 'Horizontal' AND $sidebar = 0) as active, column2 as description, column3 as icon diff --git a/examples/official-site/examples/menu_icon.sql b/examples/official-site/examples/menu_icon.sql index 7a7c97fe..e558fdb1 100644 --- a/examples/official-site/examples/menu_icon.sql +++ b/examples/official-site/examples/menu_icon.sql @@ -5,6 +5,6 @@ SELECT '/' AS link, TRUE AS fixed_top_menu, '{"title":"About","icon": "settings","submenu":[{"link":"/safety.sql","title":"Security","icon": "logout"},{"link":"/performance.sql","title":"Performance"}]}' AS menu_item, - '{"title":"Examples","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs.sql","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}' AS menu_item, - '{"title":"Examples","size":"sm","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs.sql","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}' AS menu_item, - 'Official [SQLPage](https://sql.ophir.dev) documentation' as footer; + '{"title":"Examples","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs/","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}' AS menu_item, + '{"title":"Examples","size":"sm","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs/","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}' AS menu_item, + 'Official [SQLPage](https://sql-page.com) documentation' as footer; diff --git a/examples/official-site/examples/multistep-form/index.sql b/examples/official-site/examples/multistep-form/index.sql index 45798fa4..cccaafbf 100644 --- a/examples/official-site/examples/multistep-form/index.sql +++ b/examples/official-site/examples/multistep-form/index.sql @@ -10,7 +10,7 @@ and each step of the form is shown conditionally based on the previous step. The form has a variable number of fields: after the number of adults and children are selected, a field is shown for each passenger to enter their name. -See [the SQL source on GitHub](https://github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/multistep-form) for the full code. +See [the SQL source on GitHub](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/multistep-form) for the full code. ' as contents_md; create temporary table if not exists cities as diff --git a/examples/official-site/examples/multistep-form/result.sql b/examples/official-site/examples/multistep-form/result.sql index d5f31f31..8e1985b7 100644 --- a/examples/official-site/examples/multistep-form/result.sql +++ b/examples/official-site/examples/multistep-form/result.sql @@ -40,5 +40,5 @@ SQLPage Airlines'' multi-step, conditional booking process. How is this even pos The entire form and result page are dynamically generated by a few simple SQL queries, that select graphical components from [SQLPage''s component library](/documentation.sql). -You can find the [**full source code** of this example on GitHub](https://github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/multistep-form). +You can find the [**full source code** of this example on GitHub](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/multistep-form). ' as contents_md; diff --git a/examples/official-site/examples/tabs.sql b/examples/official-site/examples/tabs.sql index 97746144..4eea6102 100644 --- a/examples/official-site/examples/tabs.sql +++ b/examples/official-site/examples/tabs.sql @@ -1,30 +1 @@ -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; - -create table if not exists tab_example_cards as -select 'Leaf' as title, 'Leaf_1_web' as img, 'f4' as prefix, 'green' as color, 'Autumn''s dance begins, Crimson leaves in breezy waltz, Nature''s fleeting art.' as description union all -select 'Caterpillar', 'Caterpillar_of_box_tree_moth%2C_Germany_2019', 'a9', 'green', 'Caterpillar crawls, silent transformation unfolds.' union all -select 'Butterfly', 'Common_brimstone_butterfly_(Gonepteryx_rhamni)_male', '6a', 'green', 'Cocoon unfolds wings, fleeting transformation.' union all -select 'Flower', 'Red-poppy-flower_-_West_Virginia_-_ForestWander', 'fd', 'red', 'Blossom in the sun, vibrant beauty blooms.' union all -select 'Bird', 'Summer_tanager_(Piranga_rubra)_male_Copan_3', 'dd', 'red', 'Winged melody soars' union all -select 'Medusa', 'Aurelia_aurita_2', '5c', 'blue', 'Mythic curse unveiled'; - -select 'tab' as component, true as center; -select 'Show all cards' as title, 'All things are beautiful' as description, '?' as link, $tab is null as active; -select format('Show %s cards', color) as title, - format('%s things are beautiful', color) as description, - format('?tab=%s', color) as link, - $tab=color as active, - case $tab when color then color end as color -- only show the color when the tab is active -from tab_example_cards -group by color; - - -select 'card' as component; -select title, description, - format('https://upload.wikimedia.org/wikipedia/commons/thumb/%s/%s/%s.jpg/640px-%s.jpg', substr(prefix,1,1), prefix, img, img) as top_image, - color, - 'https://en.wikipedia.org/wiki/File:' || img || '.jpg' as link -from tab_example_cards -where $tab is null or $tab = color; - -select 'text' as component, 'See [source code on GitHub](https://github.com/lovasoa/SQLpage/blob/main/examples/official-site/examples/tabs.sql)' as contents_md; \ No newline at end of file +select 'redirect' as component, 'tabs/' as link; \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/CRUD - Authentication.svg b/examples/official-site/examples/tabs/images/CRUD - Authentication.svg new file mode 100644 index 00000000..3e818d5b --- /dev/null +++ b/examples/official-site/examples/tabs/images/CRUD - Authentication.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/PostGIS - using sqlpage with geographic data.svg b/examples/official-site/examples/tabs/images/PostGIS - using sqlpage with geographic data.svg new file mode 100644 index 00000000..f3a064ab --- /dev/null +++ b/examples/official-site/examples/tabs/images/PostGIS - using sqlpage with geographic data.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/SQLPage developer user interface.svg b/examples/official-site/examples/tabs/images/SQLPage developer user interface.svg new file mode 100644 index 00000000..33803aab --- /dev/null +++ b/examples/official-site/examples/tabs/images/SQLPage developer user interface.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/corporate-conundrum.svg b/examples/official-site/examples/tabs/images/corporate-conundrum.svg new file mode 100644 index 00000000..aa87b88e --- /dev/null +++ b/examples/official-site/examples/tabs/images/corporate-conundrum.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/custom form component.svg b/examples/official-site/examples/tabs/images/custom form component.svg new file mode 100644 index 00000000..90491a2b --- /dev/null +++ b/examples/official-site/examples/tabs/images/custom form component.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/forms-with-multiple-steps.svg b/examples/official-site/examples/tabs/images/forms-with-multiple-steps.svg new file mode 100644 index 00000000..8490b2f8 --- /dev/null +++ b/examples/official-site/examples/tabs/images/forms-with-multiple-steps.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/image gallery with user uploads.svg b/examples/official-site/examples/tabs/images/image gallery with user uploads.svg new file mode 100644 index 00000000..8b35b5a7 --- /dev/null +++ b/examples/official-site/examples/tabs/images/image gallery with user uploads.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/microsoft sql server advanced forms.svg b/examples/official-site/examples/tabs/images/microsoft sql server advanced forms.svg new file mode 100644 index 00000000..558df3ac --- /dev/null +++ b/examples/official-site/examples/tabs/images/microsoft sql server advanced forms.svg @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SQL Server + diff --git a/examples/official-site/examples/tabs/images/mysql json handling.svg b/examples/official-site/examples/tabs/images/mysql json handling.svg new file mode 100644 index 00000000..0efc2fd0 --- /dev/null +++ b/examples/official-site/examples/tabs/images/mysql json handling.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/rich-text-editor.svg b/examples/official-site/examples/tabs/images/rich-text-editor.svg new file mode 100644 index 00000000..409c52d9 --- /dev/null +++ b/examples/official-site/examples/tabs/images/rich-text-editor.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/roundest_pokemon_rating.svg b/examples/official-site/examples/tabs/images/roundest_pokemon_rating.svg new file mode 100644 index 00000000..06609ebe --- /dev/null +++ b/examples/official-site/examples/tabs/images/roundest_pokemon_rating.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/sending emails.svg b/examples/official-site/examples/tabs/images/sending emails.svg new file mode 100644 index 00000000..a5fec0a2 --- /dev/null +++ b/examples/official-site/examples/tabs/images/sending emails.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/simple-website-example.svg b/examples/official-site/examples/tabs/images/simple-website-example.svg new file mode 100644 index 00000000..1acb7582 --- /dev/null +++ b/examples/official-site/examples/tabs/images/simple-website-example.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/splitwise.svg b/examples/official-site/examples/tabs/images/splitwise.svg new file mode 100644 index 00000000..1228970c --- /dev/null +++ b/examples/official-site/examples/tabs/images/splitwise.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/todo application (PostgreSQL).svg b/examples/official-site/examples/tabs/images/todo application (PostgreSQL).svg new file mode 100644 index 00000000..17214146 --- /dev/null +++ b/examples/official-site/examples/tabs/images/todo application (PostgreSQL).svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/images/user-authentication.svg b/examples/official-site/examples/tabs/images/user-authentication.svg new file mode 100644 index 00000000..40898e9d --- /dev/null +++ b/examples/official-site/examples/tabs/images/user-authentication.svg @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/official-site/examples/tabs/images/web servers - apache.svg b/examples/official-site/examples/tabs/images/web servers - apache.svg new file mode 100644 index 00000000..c502c5eb --- /dev/null +++ b/examples/official-site/examples/tabs/images/web servers - apache.svg @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/official-site/examples/tabs/index.sql b/examples/official-site/examples/tabs/index.sql new file mode 100644 index 00000000..b5f32855 --- /dev/null +++ b/examples/official-site/examples/tabs/index.sql @@ -0,0 +1,27 @@ +select 'http_header' as component, '; rel="canonical"' as "Link"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'SQLPage - SQL website examples', + 'description', 'These small focused examples each illustrate one feature of the SQLPage website builder.' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +select 'tab' as component, true as center; +select 'Show all examples' as title, 'All database examples' as description, '?' as link, $db is null as active; +select db_engine as title, + format('%s database examples', db_engine) as description, + format('?db=%s', db_engine) as link, + $db=db_engine as active, + case $db when db_engine then db_engine end as color +from example_cards +group by db_engine; + +select 'card' as component; +select title, description, + format('images/%s.svg', folder) as top_image, + db_engine as color, + 'https://github.com/sqlpage/SQLPage/tree/main/examples/' || folder as link +from example_cards +where $db is null or $db = db_engine; + +select 'text' as component, 'See [source code on GitHub](https://github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/tabs/)' as contents_md; diff --git a/examples/official-site/extensions-to-sql.md b/examples/official-site/extensions-to-sql.md new file mode 100644 index 00000000..36eb57af --- /dev/null +++ b/examples/official-site/extensions-to-sql.md @@ -0,0 +1,281 @@ +## How SQLPage runs your SQL + +SQLPage reads your SQL file and runs one statement at a time. For each statement, it + +- decides whether to: + - handle it inside SQLPage, or + - prepare it as a (potentially slightly modified) sql statement on the database. +- extracts values from the request to pass them as prepared statements parameters +- runs [`sqlpage.*` functions](/functions) +- passes the database results to components + +This page explains every step of the process, +with examples and details about differences between how SQLPage understands SQL and how your database does. + +## What runs where + +### Handled locally by SQLPage + +- Static simple selects (a tiny, fast subset of SELECT) +- Simple variable assignments that use only literals or variables + - All sqlpage functions + + +### Sent to your database + +Everything else: joins, subqueries, arithmetic, database functions, `SELECT @@VERSION`, `CURRENT_TIMESTAMP`, `SELECT *`, expressions, `FROM`, `WHERE`, `GROUP BY`, `ORDER BY`, `LIMIT`/`FETCH`, `WITH`, `DISTINCT`, etc. + +### Mixed statements using `sqlpage.*` functions + +[`sqlpage.*` functions](/functions.sql) are executed by SQLPage; your database never sees them. They can run: + +- Before the query, when used as values inside conditions or parameters. +- After the query, when used as top-level selected columns (applied per row). + +Examples are shown below. + +## Static simple selects + +A *static simple select* is a very restricted `SELECT` that SQLPage can execute entirely by itself. This avoids back and forths between SQLPage and the database for trivial queries. + +To be static and simple, a statement must satisfy all of the following: + +- No `FROM`, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`/`FETCH`, `WITH`, `DISTINCT`, `TOP`, windowing, locks, or other clauses. +- Each selected item is of the form `value AS alias`. +- Each `value` is either: + - a literal (single-quoted string, number, boolean, or `NULL`), or + - a variable (like `$name`, `:message`) + +That’s it. If any part is more complex, it is not a static simple select and will be sent to the database. + +#### Examples that ARE static (executed by SQLPage) + +```sql +SELECT 'text' AS component, 'Hello' AS contents; +SELECT 'text' AS component, $name AS contents; +``` + +#### Examples that are NOT static (sent to the database) + +```sql +-- Has string concatenation +select 'from' as component, 'handle_form.sql?id=' || $id as action; + +-- Has WHERE +select 'text' as component, $alert_message as contents where $should_alert; + +-- Uses database functions or expressions +SELECT 1 + 1 AS two; +SELECT CURRENT_TIMESTAMP AS now; +SELECT @@VERSION AS version; -- SQL Server variables +-- Uses a subquery +SELECT (select 1) AS one; +``` + +## Variables + +SQLPage communicates information about incoming HTTP requests to your SQL code through prepared statement variables. + +### Variable Types and Mutability + +There are three types of variables in SQLPage: + +1. `GET` variables, or **URL parameters** (immutable): + - data sent in the URL query string. For example, in `http://example.com/my_page.sql?id=123`, your SQL code would have access to `$id`. +2. `POST` variables, or **form parameters** (immutable): + - data sent in the HTTP request body. For example, submitting a form with a field named `username` would make `:username` available in your SQL code. +3. `SET` variables, or **User-defined variables** (mutable): + - Variables created and modified with the `SET` command. For example, `SET greetings = $greetings || '!'` would update the value of `$greetings`. + +`SET` variables shadow `GET` variables with the same name, but the underlying url parameter value is still accessible using [`sqlpage.variables('get')`](/functions?function=variables). + +### POST parameters + +Form fields sent with POST are available as `:name`. + +```sql +SELECT + 'form' AS component, + 'POST' AS method, + 'result.sql' AS action; + +SELECT 'age' AS name, 'How old are you?' AS label, 'number' AS type; +``` + +```sql +-- result.sql +SELECT 'text' AS component, 'You are ' || :age || ' years old!' AS contents; +``` + +### URL parameters + +Query-string parameters are available as `$name`. + +```sql +SELECT 'text' AS component, 'You are ' || $age || ' years old!' AS contents; +-- /result.sql?age=42 → You are 42 years old! +``` + +When a URL parameter is not set, its value is `NULL`. + +### The SET command + +`SET` creates or updates a user-defined variable in SQLPage (not in the database). Only strings and `NULL` are stored. + +```sql +-- Give a default value to a variable +SET post_id = COALESCE($post_id, 0); + +-- User-defined variables shadow URL parameters with the same name +SET my_var = 'custom value'; -- This value takes precedence over ?my_var=... +``` + +**Variable Lookup Precedence:** +- `$var`: checks user-defined variables first, then URL parameters +- `:var`: checks user-defined variables first, then POST parameters + +This means `SET` variables always take precedence over request parameters when using `$var` or `:var` syntax. + +**How SET works:** +- If the right-hand side is purely literals/variables, SQLPage computes it directly. See the section about *static simple select* above. +- If it needs the database (for example, calls a database function), SQLPage runs an internal `SELECT` to compute it and stores the first column of the first row of results. + +Only a single textual value (**string or `NULL`**) is stored. +`SET id = 1` will store the string `'1'`, not the number `1`. + +On databases with a strict type system, such as PostgreSQL, if you need a number, you will need to cast your variables: `SELECT * FROM post WHERE id = $id::int`. + +Complex structures can be stored as json strings. + +For larger temporary results, prefer temporary tables on your database; do not send them to SQLPage at all. + +## `sqlpage.*` functions + +Functions under the `sqlpage.` prefix run in SQLPage. See the [functions page](/functions.sql). + +They can run: + +### Before sending the query (as input values) + +Used inside conditions or parameters, the function is evaluated first and its result is passed to the database. + +```sql +SELECT * +FROM blog +WHERE slug = sqlpage.path(); +``` + +### After receiving results (as top-level selected columns) + +Used as top-level selected columns, the query is rewritten to first fetch the raw column, and the function is applied per row in SQLPage. + +```sql +SELECT sqlpage.read_file_as_text(file_path) AS contents +FROM blog_posts; +``` + +## Performance + +See the [performance page](/performance.sql) for details. In short: + +- Statements sent to the database are prepared and cached. +- Variables and pre-computed values are bound as parameters. +- This keeps queries fast and repeatable. + +## Working with larger temporary results + +### Temporary tables in your database + +When you reuse the same values multiple times in your page, +store them in a temporary table. + +```sql +DROP TABLE IF EXISTS filtered_posts; +CREATE TEMPORARY TABLE filtered_posts AS +SELECT * FROM posts where category = $category; + +select 'alert' as component, count(*) || 'results' as title +from filtered_posts; + +select 'list' as component; +select name from filtered_posts; +``` + +### Small JSON values in variables + +Useful for small datasets that you want to keep in memory. +See the [guide on JSON in SQL](/blog.sql?post=JSON+in+SQL%3A+A+Comprehensive+Guide). + +```sql +set product = ( + select json_object('name', name, 'price', price) + from products where id = $product_id +); +``` + +## CSV imports + +When you write a compatible `COPY ... FROM 'field'` statement and upload a file with the matching form field name, SQLPage orchestrates the import: + +- PostgreSQL: the file is streamed directly to the database using `COPY FROM STDIN`; the database performs the import. +- Other databases: SQLPage reads the CSV and inserts rows using a prepared `INSERT` statement. Options like delimiter, quote, header, escape, and a custom `NULL` string are supported. With a header row, column names are matched by name; otherwise, the order is used. + +Example: + +```sql +COPY my_table (col1, col2) +FROM 'my_csv' +(DELIMITER ';', HEADER); +``` + +The uploaded file should be provided in a form field with `'file' as type, 'my_csv' as name`. + +## Data types + +Each database has its own usually large set of data types. +SQLPage itself has a much more rudimentary type system. + +### From the user to SQLPage + +Form fields and URL parameters in HTTP are fundamentally untyped. +They are just sequences of bytes. SQLPage requires them to be valid utf8 strings. + +SQLPage follows the convention that when a parameter name ends with `[]`, it represents an array. +Arrays in SQLPage are represented as JSON strings. + +Example: In `users.sql?user[]=Tim&user[]=Tom`, `$user` becomes `'["Tim", "Tom"]'` (a JSON string exploitable with your database's builtin json functions). + +### From SQLPage to the database + +SQLPage sends only strings (`TEXT` or `VARCHAR`) and `NULL`s as parameters. + +### From the database to SQLPage + +Each row returned by the database becomes a JSON object +before its passed to components: + +- Each column is a key. Duplicate column names turn into arrays. +- Numbers, booleans, text, and `NULL` map naturally. +- Dates/times become ISO strings. +- Binary data (BLOBs) becomes a data URL (with mime type auto-detection). + +#### Example + +```sql +SELECT + 1 AS one, + 'x' AS my_array, 'y' AS my_array, + now() AS today, + ''::bytea AS my_image; +``` + +Produces something like: + +```json +{ + "one": 1, + "my_array": ["x", "y"], + "today": "2025-08-30T06:40:13.894918+00:00", + "my_image": "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=" +} +``` diff --git a/examples/official-site/extensions-to-sql.sql b/examples/official-site/extensions-to-sql.sql new file mode 100644 index 00000000..752eac72 --- /dev/null +++ b/examples/official-site/extensions-to-sql.sql @@ -0,0 +1,12 @@ +select 'http_header' as component, + 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'SQLPage - Extensions to SQL' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +-- Article by Matthew Larkin +select 'text' as component, + sqlpage.read_file_as_text('extensions-to-sql.md') as contents_md, + true as article; diff --git a/examples/official-site/functions.sql b/examples/official-site/functions.sql index 8cbc5c5a..ad91cfa7 100644 --- a/examples/official-site/functions.sql +++ b/examples/official-site/functions.sql @@ -1,12 +1,19 @@ -select 'dynamic' as component, - json_set( - properties, - '$[0].title', - 'SQLPage functions' || COALESCE(': ' || $function, ' documentation') - ) as properties +select 'http_header' as component, + printf('<%s>; rel="canonical"', + iif($function is not null, sqlpage.link('functions', json_object('function', $function)), 'functions.sql') + ) as "Link"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($function || ' - ', '') || 'SQLPage Functions Documentation' +)) as properties FROM example WHERE component = 'shell' LIMIT 1; -select 'text' as component, 'SQLPage built-in functions' as title; +select 'breadcrumb' as component; +select 'SQLPage' as title, '/' as link, 'Home page' as description; +select 'Functions' as title, '/functions.sql' as link, 'List of all functions' as description; +select $function as title, sqlpage.set_variable('function', $function) as link where $function IS NOT NULL; + +select 'text' as component, 'SQLPage built-in functions' as title where $function IS NULL; select ' In addition to normal SQL functions supported by your database, SQLPage provides a few special functions to help you extract data from user requests. @@ -17,20 +24,21 @@ Thus, they require all the parameters to be known at the time the query is sent Function parameters cannot reference columns from the rest of your query. The only case when you can call a SQLPage function with a parameter that is not a constant is when it appears at the top level of a `SELECT` statement. For example, `SELECT sqlpage.url_encode(url) FROM t` is allowed because SQLPage can execute `SELECT url FROM t` and then apply the `url_encode` function to each value. -' as contents_md; -select 'list' as component, 'SQLPage functions' as title; +For more information about how SQLPage functions are evaluated, and data types in SQLPage, read [the SQLPage data model documentation](/extensions-to-sql). +' as contents_md where $function IS NULL; + +select 'list' as component, 'SQLPage functions' as title where $function IS NULL; select name as title, icon, '?function=' || name || '#function' as link, $function = name as active from sqlpage_functions +where $function IS NULL order by name; -select 'text' as component, - 'The sqlpage.' || $function || ' function' as title, - 'function' as id - where $function IS NOT NULL; +select 'text' as component, 'sqlpage.' || $function || '(' || string_agg(name, ', ') || ')' as title, 'function' as id +from sqlpage_function_parameters where $function IS NOT NULL and "function" = $function; select 'text' as component; select 'Introduced in SQLPage ' || introduced_in_version || '.' as contents, 1 as size from sqlpage_functions where name = $function; @@ -45,4 +53,16 @@ select type as footer, 'azure' as color from sqlpage_function_parameters where "function" = $function -ORDER BY "index"; \ No newline at end of file +ORDER BY "index"; + +select + 'button' as component, + 'sm' as size, + 'pill' as shape; +select + name as title, + icon, + sqlpage.set_variable('function', name) as link +from sqlpage_functions +where $function IS NOT NULL +order by name; diff --git a/examples/official-site/highlightjs-launch.js b/examples/official-site/highlightjs-launch.js deleted file mode 100644 index 221b45bd..00000000 --- a/examples/official-site/highlightjs-launch.js +++ /dev/null @@ -1 +0,0 @@ -hljs.highlightAll() \ No newline at end of file diff --git a/examples/official-site/highlightjs-tabler-theme.css b/examples/official-site/highlightjs-tabler-theme.css deleted file mode 100644 index 87606b97..00000000 --- a/examples/official-site/highlightjs-tabler-theme.css +++ /dev/null @@ -1,105 +0,0 @@ -/* Comments, Prolog, Doctype, and Cdata */ -.hljs-comment, -.hljs-prolog, -.hljs-meta, -.hljs-cdata { - color: var(--tblr-gray-300); -} - -/* Punctuation */ -.hljs-template-variable, -.hljs-punctuation { - color: #e9eac7; -} - -/* Namespace */ -.hljs-namespace { - opacity: .7; -} - -/* Property and Tag */ -.hljs-property { - color: #de5f8f; -} - -/* Number */ -.hljs-number { - color: #ea9999; -} - -/* Boolean */ -.hljs-literal { - color: #ae81ff; -} - -/* Selector, Attr-name, and String */ -.hljs-attr { - color: #fcfce5; -} - -.hljs-name { - color: #e4faf6; -} -.hljs-selector-tag, -.hljs-string { - color: #97e1a3; -} - -/* Operator, Entity, URL, CSS String, and Style String */ -.hljs-operator, -.hljs-symbol, -.hljs-link, -.language-css .hljs-string, -.style .hljs-string { - color: #f8f8f2; -} - -/* At-rule and Attr-value */ -.hljs-tag, -.hljs-keyword, -.hljs-attribute-value { - color: #e6db74; -} - -/* Keyword */ -.hljs-template-tag, -.hljs-keyword { - color: #95d1ff; -} - -/* Regex and Important */ -.hljs-regexp, -.hljs-important { - color: var(--tblr-yellow); -} - -/* Important */ -.hljs-important { - font-weight: bold; -} - -/* Entity */ -.hljs-symbol { - cursor: help; -} - -/* Token transition */ -.hljs { - transition: .3s; -} - -/* Code selection */ -code::selection, code ::selection { - background: var(--tblr-yellow); - color: var(--tblr-gray-900); - border-radius: .1em; -} - -code .hljs-keyword::selection, code .hljs-punctuation::selection { - color: var(--tblr-gray-800); -} - -/* Pre code padding */ -pre code { - padding: 0; -} diff --git a/examples/official-site/index-old.sql b/examples/official-site/index-old.sql new file mode 100644 index 00000000..297d7907 --- /dev/null +++ b/examples/official-site/index-old.sql @@ -0,0 +1,232 @@ +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; + +-- Fetch the page title and header from the database +select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; + +SELECT 'hero' as component, + 'From database to data app, fast.' as title, + '**SQLPage** lets you build data-driven applications in a few SQL queries. + +It's free, [open-source](https://github.com/sqlpage/SQLPage), lightweight, and easy to use. + ' as description_md, + 'sqlpage_cover_image.webp' as image, + TRUE as rounded, + 'your-first-sql-website/' as link, + 'Build your first SQL website !' as link_text; + +-- the mantra: fast, beautiful, easy +SELECT 'Easy' as title, + 'You can teach yourself enough SQL to query and edit a database through SQLPage in a weekend. +We handle [security](safety.sql) and [performance](performance.sql) for you, so you can focus on your data.' as description_md, + 'sofa' as icon, + 'blue' as color; +SELECT 'Beautiful' as title, + 'The page you are looking at right now is written entirely in SQL. +No design skills are required, yet your website will be responsive, and look professional and modern by default.' as description, + 'eye' as icon, + 'green' as color; +SELECT 'Fast' as title, + 'Pages [load instantly](performance.sql), even on slow mobile networks. + SQLPage is designed as a single **lightweight** executable leveraging server-side rendering to ensure fast page loads even on low-cost servers.' as description_md, + 'mail-fast' as icon, + 'red' as color; + +SELECT 'hero' as component, + true as reverse, + '🧩 SQL User Interfaces' as title, + 'At the core of SQLPage is a [rich library of user interface components](/documentation.sql) for tables, charts, maps, timelines, forms and much more. + +To build your app, you just populate the components with data returned by your database queries.' as description_md, + 'sqlpage_illustration_components.webp' as image; + +SELECT 'hero' as component, + '🪄 Seamlessly' as title, + 'SQLPage handles HTTP requests, database connections, streaming rendering, styling, [security](safety.sql), and [performance](performance.sql) for you. + +Focus only on your data, and how you want to present it. We''ve tamed the tech, you tame the data.' as description_md, + 'sqlpage_illustration_alien.webp' as image; + +-- Quick feature overview +SELECT 'card' as component, + 'What is SQLPage ?' as title, + 1 as columns; +SELECT '✨ SQLPage turns your SQL queries into eye-catching websites' as title, + ' +SQLPage is a tool that allows you to **build websites** using nothing more than **SQL queries**. +You write simple text files containing SQL queries, SQLPage runs them on your database, and **renders the results as a website**. + +You can display the information you `SELECT` from your database in +lists, tables, charts, maps, forms, and many other user interface widgets. +But you can also `INSERT`, `UPDATE` and `DELETE` data from your database using SQLPage, and build a full webapp.' as description_md, + 'paint' as icon, + 'blue' as color; +SELECT '🧩 Pre-built components let you construct websites Quickly and Easily' as title, + 'At the core of SQLPage is [a rich library of **components**](./documentation.sql). + These components are built using traditional web technologies, but you never have to edit them if you don''t want to. + SQLPage populates the components with data returned by your SQL queries. + You can build entire web applications just by combining the components that come bundled with SQLPage. + +As an example, the list of features on this page is generated using a simple SQL query that looks like this: + +```sql +SELECT ''card'' as component, ''What is SQLPage ?'' as title; +SELECT header AS title, contents AS description_md FROM homepage_features; +``` + +However, you can also create your own components, or edit the existing ones to customize your website to your liking. +Creating a new component is as simple as creating an HTML template file. +' as description_md, + 'rocket' as icon, + 'green' as color; +SELECT '🛡️ A secure web server written in Rust' as title, + 'SQLPage removes a lot of the complexity and bloat of the modern web. +It''s a **lightweight web server** that just receives a request, finds the file to execute, runs it, +and returns a web page for the browser to display. + +Written in a fast and secure programming language ([**Rust**](https://en.wikipedia.org/wiki/Rust_(programming_language))), +it empowers non-developers to build secure web applications easily. You download [a single executable file](https://github.com/sqlpage/SQLPage/releases), +write an `index.sql`, and in five minutes you turned your database into a website that you can +[deploy on the internet easily](https://datapage.app). + +We made all the [optimizations](performance.sql), wrote all of the HTTP request handling code and rendering logic, +implemented all of the security features, so that you can think about your data, and nothing else. + +When SQLPage receives a request, it finds the corresponding SQL file (with or without the .sql extension), runs it on the database, passing it information from the web request as SQL statement parameters [in a safe manner](safety.sql). +When the database starts returning rows for the query, +SQLPage maps each piece of information in the row to a parameter in the template of a pre-defined component, +and streams the result back to the user's browser. +' as description_md, + 'server' as icon, + 'purple' as color; +SELECT '🌱 Start Simple, Scale to Advanced' as title, + 'SQLPage is a great starting point for building websites, internal tools, dashboards and data applications, +especially if you don''t have a lot of time, but need something more powerful and user-friendly than a spreadsheet. + +When your app grows, you can take the same underlying data structure and queries, +and wrap them in a more established framework with a dedicated front end if you need to. +There is no lock-in, only simple, standard SQL queries that run directly on your database. + +**Focus on what matters** first: your data and your users. Not centering boxes in CSS, not setting up a web framework.' as description_md, + 'world-cog' as icon, + 'orange' as color; + +-- Useful links +SELECT 'list' as component, + 'Get started: where to go from here ?' as title, + 'Here are some useful links to get you started with SQLPage.' as description; +SELECT 'Download' as title, + 'https://github.com/sqlpage/SQLPage/releases' as link, + 'SQLPage is distributed as a single binary that you can execute locally or on a web server to get started quickly.' as description, + 'green' as color, + 'download' as icon; +SELECT 'Tutorial' as title, + 'get started.sql' as link, + 'A short tutorial that will guide you through the creation of your first SQL-only website.' as description, + 'orange' as color, + 'book' as icon, + TRUE as active; +SELECT 'SQLPage Documentation' as title, + 'documentation.sql' as link, + 'List of all available components, with examples of how to use them.' as description, + 'purple' as color, + 'book' as icon; +SELECT 'Technical documentation on Github' as title, + 'https://github.com/sqlpage/SQLPage/blob/main/README.md#sqlpage' as link, + 'The official README file on Github contains instructions to get started using SQLPage.' as description, + 'yellow' as color, + 'file-text' as icon; +SELECT 'Youtube Video Series' as title, + 'https://www.youtube.com/playlist?list=PLTue_qIAHxAf9fEjBY2CN0N_5XOiffOk_' as link, + 'A series of video tutorials that will guide you through the creation of an application with SQLPage.' as description, + 'red' as color, + 'brand-youtube' as icon; +SELECT 'Learnsqlpage.com' as title, + 'https://learnsqlpage.com' as link, + 'A website dedicated to learning SQLPage, with detailed tutorials.' as description, + 'blue' as color, + 'globe' as icon; + +SELECT 'list' as component, 'Examples' as title; +SELECT 'Github Examples' as title, + 'https://github.com/sqlpage/SQLPage/tree/main/examples/' as link, + 'SQL source code for examples and demos of websites built with SQLPage.' as description, + 'teal' as color, + 'code' as icon; +SELECT 'Corporate Conundrum' as title, + 'https://conundrum.ophir.dev' as link, + 'A demo web application powered by SQLPage, designed for playing a fun trivia board game with friends.' as description, + 'cyan' as color, + 'affiliate' as icon; + +SELECT 'list' as component, 'Community' as title; +SELECT 'Discussion forum' as title, + 'https://github.com/sqlpage/SQLPage/discussions' as link, + 'Come to our community page to discuss SQLPage with other users and ask questions.' as description, + 'pink' as color, + 'user-heart' as icon; +-- github link +SELECT 'Source code' as title, + 'https://github.com/sqlpage/SQLPage' as link, + 'The rust source code for SQLPage itself is open and available on Github.' as description, + 'github' as color, + 'brand-github' as icon; +SELECT 'Report a bug, make a suggestion' as title, + 'https://github.com/sqlpage/SQLPage/issues' as link, + 'If you have a question, a suggestion, or if you found a bug, please open an issue on Github.' as description, + 'red' as color, + 'bug' as icon; + +-- User personas: who is SQLPage for ? +SELECT 'card' as component, + 'Is SQLPage for you ?' as title, + ' +SQLPage empowers SQL-savvy individuals to create dynamic websites without complex programming. + + - If you are looking to quickly build something simple yet dynamic, SQLPage is for you. + - If you want to customize how every pixel of your website looks, SQLPage is not for you. + +Compared to other low-code platforms, SQLPage focuses on SQL-driven development, more lightweight performance, and total openness. +Where other platforms try to lock you in, SQLPage makes it trivial to switch to something else when your application grows.' as description_md, + 4 as columns; +SELECT 'Business Analyst' as title, + 'Replace static dashboards with dynamic websites' as description, + 'Business analysts can leverage SQLPage to create interactive and real-time data visualizations, replacing traditional static dashboards and enabling more dynamic and insightful reporting.' as footer, + 'green' as color, + 'chart-arrows-vertical' as icon; +SELECT 'Data Scientist' as title, + 'Prototype and share data-driven experiments and analysis' as description, + 'Data scientists can utilize SQLPage to quickly prototype and share their data-driven experiments and analysis by creating interactive web applications directly from SQL queries, enabling collaboration and faster iterations.' as footer, + 'purple' as color, + 'square-root-2' as icon; +SELECT 'Marketer' as title, + 'Create dynamic landing pages and personalized campaigns' as description, + 'Marketers can leverage SQLPage to create dynamic landing pages and personalized campaigns by fetching and displaying data from databases, enabling targeted messaging and customized user experiences.' as footer, + 'orange' as color, + 'message-circle-dollar' as icon; +SELECT 'Engineer' as title, + 'Build internal tools and admin panels with ease' as description, + 'Engineers can use SQLPage to build internal tools and admin panels, utilizing their SQL skills to create custom interfaces and workflows, streamlining processes and improving productivity.' as footer, + 'blue' as color, + 'settings' as icon; +SELECT 'Product Manager' as title, + 'Create interactive prototypes and mockups' as description, + 'Product managers can leverage SQLPage to create interactive prototypes and mockups, allowing stakeholders to experience and provide feedback on website functionalities before development, improving product design and user experience.' as footer, + 'red' as color, + 'cube-send' as icon; +SELECT 'Educator' as title, + 'Develop interactive learning materials and exercises' as description, + 'Educators can utilize SQLPage to develop interactive learning materials and exercises, leveraging SQLPage components to present data and engage students in a dynamic online learning environment.' as footer, + 'yellow' as color, + 'school' as icon; +SELECT 'Researcher' as title, + 'Create data-driven websites to share findings and insights' as description, + 'Researchers can use SQLPage to create data-driven websites, making complex information more accessible and interactive for the audience, facilitating knowledge dissemination and engagement.' as footer, + 'cyan' as color, + 'flask-2' as icon; +SELECT 'Startup Founder' as title, + 'Quickly build a Minimum Viable Product' as description, + 'Startup founders can quickly build a Minimum Viable Product (MVP) using their SQL expertise with SQLPage, creating a functional website with database integration to validate their business idea and gather user feedback.' as footer, + 'pink' as color, + 'rocket' as icon; diff --git a/examples/official-site/index.sql b/examples/official-site/index.sql index d31351b9..dabd0791 100644 --- a/examples/official-site/index.sql +++ b/examples/official-site/index.sql @@ -1,206 +1,5 @@ -select 'http_header' as component, 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; --- Fetch the page title and header from the database -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; - -SELECT 'hero' as component, - 'SQLPage' as title, - 'Build **Web Apps** Effortlessly with **Only SQL Skills** - -Open-source *low-code* web application server' as description_md, - 'sqlpage_introduction_video.webm' as video, - TRUE as rounded, - 'your-first-sql-website/' as link, - 'Build your first SQL website now !' as link_text; - --- the mantra: fast, beautiful, easy -SELECT 'Easy' as title, - 'You can teach yourself enough SQL to query and edit a database through SQLPage in a weekend. -We handle [security](safety.sql) and [performance](performance.sql) for you, so you can focus on your data.' as description_md, - 'sofa' as icon, - 'blue' as color; -SELECT 'Beautiful' as title, - 'The page you are looking at right now is written entirely in SQL. -No design skills are required, yet your website will be responsive, and look professional and modern by default.' as description, - 'eye' as icon, - 'green' as color; -SELECT 'Fast' as title, - 'Pages [load instantly](performance.sql), even on slow mobile networks. - SQLPage is designed as a single **lightweight** executable, ensuring fast performance even on low-cost servers.' as description_md, - 'mail-fast' as icon, - 'red' as color; - --- Quick feature overview -SELECT 'card' as component, - 'What is SQLPage ?' as title, - 1 as columns; -SELECT 'SQLPage transforms your SQL queries into stunning websites' as title, - ' -SQLPage is a tool that allows you to **build websites** using nothing more than **SQL queries**. -You write simple text files containing SQL queries, SQLPage runs them on your database, and **renders the results as a website**. - -You can display the information you `SELECT` from your database in -lists, tables, charts, maps, forms, and many other user interface widgets. -But you can also `INSERT`, `UPDATE` and `DELETE` data from your database using SQLPage, and build a full webapp.' as description_md, - 'paint' as icon, - 'blue' as color; -SELECT 'Pre-built components let you construct websites Quickly and Easily' as title, - 'At the core of SQLPage is [a rich library of **components**](./documentation.sql). - These components are built using traditional web technologies, but you never have to edit them if you don''t want to. - SQLPage populates the components with data returned by your SQL queries. - You can build entire web applications just by combining the components that come bundled with SQLPage. - -As an example, the list of features on this page is generated using a simple SQL query that looks like this: - -```sql -SELECT ''card'' as component, ''What is SQLPage ?'' as title; -SELECT header AS title, contents AS description_md FROM homepage_features; -``` - -However, you can also create your own components, or edit the existing ones to customize your website to your liking. -Creating a new component is as simple as creating an HTML template file. -' as description_md, - 'rocket' as icon, - 'green' as color; -SELECT 'Technically, it''s just a good old web server' as title, - ' -The principles behind SQLPage are not too far from those that powered the early days of the internet. -Like [PHP](https://en.wikipedia.org/wiki/PHP), SQLPage just receives a request, finds the file to execute, runs it, -and returns a web page for the browser to display. - -SQLPage is a *web server* written in a fast and secure programming language: -[**Rust**](https://en.wikipedia.org/wiki/Rust_(programming_language)). -It is extremely easy to use: -you [download a single executable file](https://github.com/lovasoa/SQLpage/releases), -write an `.sql` file, and you''re done. -We made all the [optimizations](performance.sql), wrote all of the HTTP request handling code and rendering logic, -implemented all of the security features, so that you can think about your data, and nothing else. - -When SQLPage receives a request with a URL ending in `.sql`, it finds the corresponding -SQL file, runs it on the database, passing it information from the web request as SQL statement parameters -[in a safe manner](safety.sql). -When the database starts returning rows for the query, -SQLPage maps each piece of information in the row to a parameter in the template of a pre-defined component, -and streams the result back to the user''s browser. -' as description_md, - 'server' as icon, - 'purple' as color; -SELECT 'Start Simple, Scale to Advanced' as title, - 'SQLPage is a great starting point for building websites, especially if you''re new to coding, or want to test out a new idea quickly. - Then if the app becomes important, you can take the same underlying data structure and wrap it in a more established framework with a dedicated front end. - And if it doesn''t, you only spent a few hours on it! - - SQLPage does not impose any specific database structure, allowing for seamless integration with other tools and frameworks. - SQLPage is a solid foundation for your website development, because it lets you focus on what matters at the beginning, without closing the door to future improvements.' as description, - 'world-cog' as icon, - 'orange' as color; - --- Useful links -SELECT 'list' as component, - 'Get started: where to go from here ?' as title, - 'Here are some useful links to get you started with SQLPage.' as description; -SELECT 'Download' as title, - 'https://github.com/lovasoa/SQLpage/releases' as link, - 'SQLPage is distributed as a single binary that you can execute locally or on a web server to get started quickly.' as description, - 'green' as color, - 'download' as icon; -SELECT 'Tutorial' as title, - 'get started.sql' as link, - 'A short tutorial that will guide you through the creation of your first SQL-only website.' as description, - 'orange' as color, - 'book' as icon, - TRUE as active; -SELECT 'SQLPage Documentation' as title, - 'documentation.sql' as link, - 'List of all available components, with examples of how to use them.' as description, - 'purple' as color, - 'book' as icon; -SELECT 'Examples' as title, - 'https://github.com/lovasoa/SQLpage/tree/main/examples/' as link, - 'SQL source code for examples and demos of websites built with SQLPage.' as description, - 'teal' as color, - 'code' as icon; -SELECT 'Community' as title, - 'https://github.com/lovasoa/SQLpage/discussions' as link, - 'Come to our community page to discuss SQLPage with other users and ask questions.' as description, - 'pink' as color, - 'user-heart' as icon; --- github link -SELECT 'Source code' as title, - 'https://github.com/lovasoa/SQLPage' as link, - 'The rust source code for SQLPage itself is open and available on Github.' as description, - 'github' as color, - 'brand-github' as icon; -SELECT 'Technical documentation on Github' as title, - 'https://github.com/lovasoa/SQLpage/blob/main/README.md#sqlpage' as link, - 'The official README file on Github contains instructions to get started using SQLPage.' as description, - 'yellow' as color, - 'file-text' as icon; -SELECT 'Report a bug, make a suggestion' as title, - 'https://github.com/lovasoa/SQLPage/issues' as link, - 'If you have a question, a suggestion, or if you found a bug, please open an issue on Github.' as description, - 'red' as color, - 'bug' as icon; -SELECT 'Official website' as title, - 'https://sql.ophir.dev' as link, - 'The project''s official home page.' as description, - 'blue' as color, - 'home' as icon; -SELECT 'Corporate Conundrum' as title, - 'https://conundrum.ophir.dev' as link, - 'A demo web application powered by SQLPage, designed for playing a fun trivia board game with friends.' as description, - 'cyan' as color, - 'affiliate' as icon; - --- User personas: who is SQLPage for ? -SELECT 'card' as component, - 'Is SQLPage for you ?' as title, - ' -SQLPage empowers SQL-savvy individuals to create dynamic websites without complex programming. - - - If you are looking to quickly build something simple yet dynamic, SQLPage is for you. - - If you want to customize how every pixel of your website looks, SQLPage is not for you. - -Compared to other low-code platforms, SQLPage focuses on SQL-driven development, more lightweight performance, and total openness. -Where other platforms try to lock you in, SQLPage makes it trivial to switch to something else when your application grows.' as description_md, - 4 as columns; -SELECT 'Business Analyst' as title, - 'Replace static dashboards with dynamic websites' as description, - 'Business analysts can leverage SQLPage to create interactive and real-time data visualizations, replacing traditional static dashboards and enabling more dynamic and insightful reporting.' as footer, - 'green' as color, - 'chart-arrows-vertical' as icon; -SELECT 'Data Scientist' as title, - 'Prototype and share data-driven experiments and analysis' as description, - 'Data scientists can utilize SQLPage to quickly prototype and share their data-driven experiments and analysis by creating interactive web applications directly from SQL queries, enabling collaboration and faster iterations.' as footer, - 'purple' as color, - 'square-root-2' as icon; -SELECT 'Marketer' as title, - 'Create dynamic landing pages and personalized campaigns' as description, - 'Marketers can leverage SQLPage to create dynamic landing pages and personalized campaigns by fetching and displaying data from databases, enabling targeted messaging and customized user experiences.' as footer, - 'orange' as color, - 'message-circle-dollar' as icon; -SELECT 'Engineer' as title, - 'Build internal tools and admin panels with ease' as description, - 'Engineers can use SQLPage to build internal tools and admin panels, utilizing their SQL skills to create custom interfaces and workflows, streamlining processes and improving productivity.' as footer, - 'blue' as color, - 'settings' as icon; -SELECT 'Product Manager' as title, - 'Create interactive prototypes and mockups' as description, - 'Product managers can leverage SQLPage to create interactive prototypes and mockups, allowing stakeholders to experience and provide feedback on website functionalities before development, improving product design and user experience.' as footer, - 'red' as color, - 'cube-send' as icon; -SELECT 'Educator' as title, - 'Develop interactive learning materials and exercises' as description, - 'Educators can utilize SQLPage to develop interactive learning materials and exercises, leveraging SQLPage components to present data and engage students in a dynamic online learning environment.' as footer, - 'yellow' as color, - 'school' as icon; -SELECT 'Researcher' as title, - 'Create data-driven websites to share findings and insights' as description, - 'Researchers can use SQLPage to create data-driven websites, making complex information more accessible and interactive for the audience, facilitating knowledge dissemination and engagement.' as footer, - 'cyan' as color, - 'flask-2' as icon; -SELECT 'Startup Founder' as title, - 'Quickly build a Minimum Viable Product' as description, - 'Startup founders can quickly build a Minimum Viable Product (MVP) using their SQL expertise with SQLPage, creating a functional website with database integration to validate their business idea and gather user feedback.' as footer, - 'pink' as color, - 'rocket' as icon; +select 'shell-home' as component; diff --git a/examples/official-site/llms.txt.sql b/examples/official-site/llms.txt.sql new file mode 100644 index 00000000..a1f5cda5 --- /dev/null +++ b/examples/official-site/llms.txt.sql @@ -0,0 +1,145 @@ +select + 'http_header' as component, + 'text/markdown; charset=utf-8' as "Content-Type", + 'inline; filename="llms.txt"' as "Content-Disposition"; + +select + 'shell-empty' as component, + '# SQLPage + +> SQLPage is a SQL-only web application framework. It lets you build entire websites and web applications using nothing but SQL queries. Write `.sql` files, and SQLPage executes them, maps results to UI components (handlebars templates), and streams HTML to the browser. + +SQLPage is designed for developers who are comfortable with SQL but want to avoid the complexity of traditional web frameworks. It works with SQLite, PostgreSQL, MySQL, and Microsoft SQL Server, and through ODBC with any other database that has an ODBC driver installed. + +Key features: +- No backend code needed: Your SQL files are your backend +- Component-based UI: Built-in components for forms, tables, charts, maps, and more +- Database-first: Every HTTP request triggers a sequence of SQL queries from a .sql file, the results are rendered with built-in or custom components, defined as .handlebars files in the sqlpage/templates folder. +- Simple deployment: Single binary with no runtime dependencies +- Secure by default: Parameterized queries prevent SQL injection + +## Getting Started + +- [Introduction to SQLPage: installation, guiding principles, and a first example](/your-first-sql-website/tutorial.md): Complete beginner tutorial covering setup, database connections, forms, and deployment + +## Core Documentation + +- [Components reference](/documentation.sql): List of all ' || ( + select + count(*) + from + component + ) || ' built-in UI components with parameters and examples +- [Functions reference](/functions.sql): SQLPage built-in functions for handling requests, encoding data, and more +- [Configuration guide](https://github.com/sqlpage/SQLPage/blob/main/configuration.md): Complete list of configuration options in sqlpage.json + +## Components + +' || ( + select + group_concat ( + '### [' || c.name || '](/component.sql?component=' || c.name || ') + +' || c.description || ' + +' || ( + select + case when exists ( + select + 1 + from + parameter + where + component = c.name + and top_level + ) then '#### Top-level parameters + +' || group_concat ( + '- `' || name || '` (' || type || ')' || case when not optional then ' **REQUIRED**' else '' end || ': ' || description, + char(10) + ) + else + '' + end + from + parameter + where + component = c.name + and top_level + ) || ' + +' || ( + select + case when exists ( + select + 1 + from + parameter + where + component = c.name + and not top_level + ) then '#### Row-level parameters + +' || group_concat ( + '- `' || name || '` (' || type || ')' || case when not optional then ' **REQUIRED**' else '' end || ': ' || description, + char(10) + ) + else + '' + end + from + parameter + where + component = c.name + and not top_level + ) || ' + +', + '' + ) + from + component c + order by + c.name + ) || ' + +## Functions + +' || ( + select + group_concat ( + '### [sqlpage.' || name || '()](/functions.sql?function=' || name || ') +' || replace ( + replace ( + description_md, + char(10) || '#', + char(10) || '###' + ), + ' ', + ' ' + ), + char(10) + ) + from + sqlpage_functions + order by + name + ) || ' + +## Examples + +- [Authentication example](https://github.com/sqlpage/SQLPage/tree/main/examples/user-authentication): Complete user registration and login system +- [CRUD application](https://github.com/sqlpage/SQLPage/tree/main/examples/CRUD%20-%20Authentication): Create, read, update, delete with authentication +- [Image gallery](https://github.com/sqlpage/SQLPage/tree/main/examples/image%20gallery%20with%20user%20uploads): File upload and image display +- [Todo application](https://github.com/sqlpage/SQLPage/tree/main/examples/todo%20application): Simple CRUD app +- [Master-detail forms](https://github.com/sqlpage/SQLPage/tree/main/examples/master-detail-forms): Working with related data +- [Charts example](https://github.com/sqlpage/SQLPage/tree/main/examples/plots%20tables%20and%20forms): Data visualization + +## Optional + +- [Custom components guide](/custom_components.sql): Create your own handlebars components +- [Safety and security](/safety.sql): Understanding SQL injection prevention +- [Docker deployment](https://github.com/sqlpage/SQLPage#with-docker): Running SQLPage in containers +- [Systemd service](https://github.com/sqlpage/SQLPage/blob/main/sqlpage.service): Production deployment setup +- [Repository structure](https://github.com/sqlpage/SQLPage/blob/main/CONTRIBUTING.md): Project organization and contribution guide +' as html; \ No newline at end of file diff --git a/examples/official-site/performance.sql b/examples/official-site/performance.sql index 4a9e5834..332f6e3b 100644 --- a/examples/official-site/performance.sql +++ b/examples/official-site/performance.sql @@ -1,4 +1,16 @@ -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'SQLPage applications are fast' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +select 'hero' as component, + 'Performance in SQLPage' as title, + 'SQLPage applications are fast, because they are server-side rendered, and begin streaming the page to the browser while the database is still processing the request.' as description, + 'performance.webp' as image; select 'text' as component, ' @@ -10,13 +22,13 @@ as opposed to writing imperative code in a backend programming language like Jav This declarative approach allows SQLPage to offer **optimizations** out of the box that are difficult or time-consuming to achieve in traditional web development stacks. -## Server-side rendering +## Progressive server-side rendering SQLPage applications are [server-side rendered](https://web.dev/articles/rendering-on-the-web), -which means that the SQL queries are executed on the server, and the results are sent to the user''s browser -as HTML, which allows it to start rendering the page as soon as the first byte is received. +which means that the SQL queries are executed on the server, and the results are sent to the user''s browser as HTML. In contrast, many other web frameworks render the page on the client side, which means that the browser has to download some HTML, then download some JavaScript, then execute the JavaScript, then make more requests, +wait for the database to produce a full result set, then process the responses before it can start rendering the actual data the user is interested in. This can lead to loading times that are several times longer than a SQLPage application. @@ -24,6 +36,7 @@ This can lead to loading times that are several times longer than a SQLPage appl SQLPage applications will often feel faster than even equivalent applications written even in alternative server-side rendering frameworks, because SQLPage streams the results of the SQL queries to the browser as soon as they are available. +The user sees the start of the page even before the database has finished producing the last query results. Most server-side rendering frameworks will first wait for all the SQL queries to finish, then render the page in memory on the server, and only then send the HTML webpage to the browser. If a page contains a long list of items, the user @@ -39,7 +52,7 @@ an execution plan every time an user requests a page. When an user loads a page, all SQLPage has to do is tell the database: "Hey, do you remember that query we talked about earlier? Can you give me the results for these specific parameters?". This is much faster than sending the whole SQL query -string to the database every time. +string to the database every time, especially for large complex queries that require heavy planning on the database side. ## Compiled templates @@ -75,16 +88,15 @@ interaction. ## Key Takeaways -SQLPage offers a radically different approach to web development, -resolving the classical tension between performance and ease of use. - -By leveraging a declarative approach, server-side rendering, and advanced optimization techniques, SQLPage enables: - -* **Faster page loads**: Long loading times make your website feel sluggish and unresponsive, causing users to leave. -* **Easier development**: Focus on writing SQL queries; all the heavy lifting is done for you. -* **Cost effective**: SQLPage''s low CPU and memory usage means you can host your website extremely cheaply, even if it gets significant traffic. +Performance is a key feature of SQLPage. +Its architecture allows you to build fast websites without having to implement advanced optimizations yourself. ## Ready to get started? [Build your fast, secure, and beautiful website](/your-first-sql-website) with SQLPage today! + +## Already a SQLPage developer ? + +Have a look at our [performance guide](/blog?post=Performance+Guide) to learn the best practices to leverage +all the features that will make your site faster. ' as contents_md; diff --git a/examples/official-site/performance.webp b/examples/official-site/performance.webp new file mode 100644 index 00000000..60cebbc6 Binary files /dev/null and b/examples/official-site/performance.webp differ diff --git a/examples/official-site/pgconf/2024-sqlpage-badass.pdf b/examples/official-site/pgconf/2024-sqlpage-badass.pdf new file mode 100644 index 00000000..46918363 Binary files /dev/null and b/examples/official-site/pgconf/2024-sqlpage-badass.pdf differ diff --git a/examples/official-site/pgconf/pgconf-2023.html b/examples/official-site/pgconf/pgconf-2023.html index 4eeb83b6..f87302f9 100644 --- a/examples/official-site/pgconf/pgconf-2023.html +++ b/examples/official-site/pgconf/pgconf-2023.html @@ -570,7 +570,7 @@

1

1

-
+
@@ -1427,7 +1427,7 @@

SQLPage in Action

}
http://localhost/api.sql
-
docker run -p 80:8080 -v .:/var/www lovasoa/sqlpage
+
docker run -p 80:8080 -v .:/var/www sqlpage/SQLPage

Examples

@@ -1474,9 +1474,9 @@

SQLPage in Action

-

lovasoa/SQLPage

+

sqlpage/SQLPage

-

sql.ophir.dev

+

sql-page.com

Visit the website

diff --git a/examples/official-site/pricing.sql b/examples/official-site/pricing.sql new file mode 100644 index 00000000..0a52fa78 --- /dev/null +++ b/examples/official-site/pricing.sql @@ -0,0 +1,79 @@ +SELECT 'shell' as component, +'style_pricing.css' as css ; + + +SELECT 'hero' as component, + 'DATAPAGE PRICING PLANS' as title, +' +> *Start free, launch with fixed costs, and scale efficiently.* + +> If you have any questions regarding **DataPage.app**, fill out the form [*here*](https://beta.datapage.app/fill-the-form.sql) and we''ll get back to you shortly.' as description_md; + +SELECT 'START PLAN' as title, +' +### **Price**: **€18/month** *(First 1 month FREE)* +### **🚩[Register for the *START Plan*](https://buy.stripe.com/9AQeWCa6k85Q9gY8wy)** +--- +- **Database Size**: **128MB** +- **Ideal For**: Testing and small-scale projects. +- **Features**: + - Basic SQLPage hosting. + - Essential components for simple applications. + - Community Support via forums. +--- +### **🚩[Register for the *START Plan*](https://buy.stripe.com/9AQeWCa6k85Q9gY8wy)** +' +as description_md, + 'player-play' as icon, + 'blue' as color; + +SELECT 'PRO PLAN' as title, +' +### **Price**: **€40/month** *(First 1 month FREE)* +### **🚩[Register for the *PRO Plan*](https://buy.stripe.com/eVabKqces99U1OweUX)** +--- +- **Database Size**: **1GB** +- **Ideal For**: Growing projects and businesses needing enhanced support and features +- **Features**: + - All *START plan* features. + - **Priority support**: Get faster response times and direct assistance from our support team + - **Custom Domain**: Use your custom domain name with your SQLPage app +--- + +### **🚩[Register for the *PRO Plan*](https://buy.stripe.com/eVabKqces99U1OweUX)** +' + as description_md, + 'shield-check' as icon, + 'green' as color; + + + + +SELECT 'ENTREPRISE PLAN' as title, +' +### **Price**: **€600/month** *(First 1 month FREE)* +### **🚩[Register for the *ENTREPRISE Plan*](https://buy.stripe.com/8wM6q62DS5XI3WE4gk)** +--- +- **Database**: **Custom Scaling** +- **Ideal For**: Large-scale operations with custom needs. +- **Features**: + - All Pro Plan features. + - **Custom Deployment**: Tailored deployment to suit your specific requirements, whether on-premises or in the cloud. + - **Database Scaling**: Dynamically scale your database to handle increased traffic and storage needs. + - **Authentication**: Implement OpenID Connect and OAuth2 for secure user authentication via Google, Facebook, or internal company accounts. + - **Premium Components**: Access to exclusive, high-performance components for building complex applications. + - **1-Hour Monthly Support**: Dedicated one-on-one support session with our experts each month. + - **SLA Agreement**: Service Level Agreement with guaranteed uptime and response times. + - **Custom Integration**: Personalized integration with your existing systems and workflows. + - **Onboarding Assistance**: Get personalized setup and onboarding assistance for a smooth start. +--- + +### **🚩[Register for the *ENTREPRISE Plan*](https://buy.stripe.com/8wM6q62DS5XI3WE4gk)** + ' as description_md, + 'bubble-plus' as icon, + 'red' as color; + +SELECT 'text' as component, +'' as title, +'## **Ready to Get Started?** +[Sign Up Now](https://datapage.app) and start building your SQLPage app with Datapage.app today!' as contents_md; diff --git a/examples/official-site/robots.txt b/examples/official-site/robots.txt new file mode 100644 index 00000000..657f911e --- /dev/null +++ b/examples/official-site/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: /examples/authentication/basic_auth.sql +Disallow: /cdn-cgi/l/email-protection +Disallow: /sqlpage/ +Crawl-delay: 1 diff --git a/examples/official-site/rss.sql b/examples/official-site/rss.sql index 3bbe4153..c23d07ca 100644 --- a/examples/official-site/rss.sql +++ b/examples/official-site/rss.sql @@ -3,17 +3,17 @@ select 'http_header' as component, select 'shell-empty' as component; select 'rss' as component, 'SQLPage blog' as title, - 'https://sql.ophir.dev/blog.sql' as link, + 'https://sql-page.com/blog.sql' as link, 'latest news about SQLpage' as description, 'en' as language, - 'https://sql.ophir.dev/rss.sql' as self_link, + 'https://sql-page.com/rss.sql' as self_link, 'Technology' as category, '2de3f968-9928-5ec6-9653-6fc6fe382cfd' as guid; SELECT title, description, CASE WHEN external_url IS NOT NULL THEN external_url - ELSE 'https://sql.ophir.dev/blog.sql?post=' || title + ELSE 'https://sql-page.com/blog.sql?post=' || title END AS link, created_at AS date, false AS explicit diff --git a/examples/official-site/safety.sql b/examples/official-site/safety.sql index cca3709d..a9af1f14 100644 --- a/examples/official-site/safety.sql +++ b/examples/official-site/safety.sql @@ -1,9 +1,19 @@ -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'Security in SQLPage: SSO, protection against SQLi, XSS, CSRF, and more' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +select 'hero' as component, + 'SQLPage''s security guarantees' as title, + 'SQLPage prevents common web vulnerabilities such as SQL injections and XSS attacks by default.' as description, + 'safety.webp' as image; select 'text' as component, ' -# SQLPage''s security guarantees - SQLPage is a tool that allows you to create a full website using only SQL queries, and render results straight from the database to the browser. Most programmers, hearing this, will immediately think of the security implications of this model. @@ -18,7 +28,9 @@ SQLPage websites are *server-side rendered*, which means that the SQL queries st where SQLPage is installed. The results of these queries are then rendered to HTML, and sent to the user''s browser. -A malicious user cannot run arbitrary SQL queries on your database, because SQLPage does not expose your database to the internet. +A malicious user cannot run arbitrary SQL queries on your database, because SQLPage +does not expose your entire database to the internet, only the results of +your prepared queries, rendered as web pages. ## Protection against SQL injections @@ -77,23 +89,35 @@ parameter of the [`shell`](documentation.sql?component=shell#component) componen ## Authentication -SQLPage provides an [authentication](/documentation.sql?component=authentication#component) component that allows you to -restrict access to some pages of your website to authenticated users. +Use either the built-in username/password or Single Sign-On; both follow safe defaults. + +### Built-in username/password + +SQLPage provides an [authentication](/documentation.sql?component=authentication#component) component to protect pages, +with helpers like [`sqlpage.basic_auth_username()`](/functions.sql?function=basic_auth_username#function), +[`sqlpage.basic_auth_password()`](/functions.sql?function=basic_auth_password#function), and +[`sqlpage.hash_password()`](/functions.sql?function=hash_password#function). +Passwords are salted and hashed with [argon2](https://en.wikipedia.org/wiki/Argon2), +following [best practices](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html). + +### Session management + +If you implement your own sessions using the [`cookie` component](/documentation.sql?component=cookie#component), +follow the [OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#cookies). +Avoid rolling your own unless you fully understand web security. -It also provides useful built-in functions such as -[`sqlpage.basic_auth_username()`](/functions.sql?function=basic_auth_username#function), -[`sqlpage.basic_auth_password()`](/functions.sql?function=basic_auth_password#function) and -[`sqlpage.hash_password()`](/functions.sql?function=hash_password#function) -to help you implement your authentication system entirely in SQL. +### Single Sign-On (OIDC) -The components and functions provided by SQLPage are designed to be used by non-technical users, -and to respect [security best practices](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) by default. -Passwords are [hashed with a salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) using the -[argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. +When OIDC is enabled, SQLPage validates a signed identity token on every request +before any of your SQL runs. Without a successful login, requests are redirected +to your identity provider and your application code never executes. +This keeps attackers outside your SSO realm from reaching your app, +even if a vulnerability exists in your own code. -However, if you implement your own session management system using the [`cookie` component](/documentation.sql?component=cookie#component), -you should be careful to follow the [OWASP session management best practices](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#cookies). -Implementing your own session management system is not recommended if you are a non-technical user and don''t have a good understanding of web security. +By default, all pages are protected when single sign-on is enabled. +Once authenticated, you can access user claims with +[`sqlpage.user_info()`](/functions.sql?function=user_info) +to further restrict what users see based on who they are. ## Protection against [CSRF attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery) @@ -118,12 +142,12 @@ Advanced users who may want to implement their own csrf protection system can do using the [`sqlpage.random_string()`](/functions.sql?function=random_string#function) function, and the `hidden` input type of the [`form`](/documentation.sql?component=form#component) component. -For more information, see the [this discussion](https://github.com/lovasoa/SQLpage/discussions/148). +For more information, see the [this discussion](https://github.com/sqlpage/SQLPage/discussions/148). ## Database connections SQLPage uses a fixed pool of database connections, and will never open more connections than the ones you -[configured](https://github.com/lovasoa/SQLpage/blob/main/configuration.md). So even under heavy load, your database +[configured](https://github.com/sqlpage/SQLPage/blob/main/configuration.md). So even under heavy load, your database connection limit will never be saturated by SQLPage. And SQLPage will accept any restriction you put on the database user you use to connect to your database, so you can diff --git a/examples/official-site/safety.webp b/examples/official-site/safety.webp new file mode 100644 index 00000000..11ea6845 Binary files /dev/null and b/examples/official-site/safety.webp differ diff --git a/examples/official-site/search.sql b/examples/official-site/search.sql new file mode 100644 index 00000000..2f5232a8 --- /dev/null +++ b/examples/official-site/search.sql @@ -0,0 +1,117 @@ +set search = nullif(trim($search), ''); + +-- Check for exact matches and redirect if found +set redirect = CASE + WHEN EXISTS (SELECT 1 FROM component WHERE name = $search) THEN sqlpage.link('/component.sql', json_object('component', $search)) + WHEN EXISTS (SELECT 1 FROM sqlpage_functions WHERE name = $search) THEN sqlpage.link('/functions.sql', json_object('function', $search)) +END +SELECT 'redirect' as component, $redirect as link WHERE $redirect IS NOT NULL; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', coalesce($search || ' | ', '') || 'SQLPage documentation search' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +SELECT 'form' as component, + 'GET' as method, + true as auto_submit, + 'Search documentation' as title; + +SELECT 'text' as type, + 'search' as name, + '' as label, + true as autofocus, + 'Search for components, parameters, functions...' as placeholder, + $search as value; + +set escaped_search = '"' || replace($search, '"', '""') || '"'; + +SELECT 'text' as component, + CASE + WHEN $search IS NULL THEN 'Enter a search term above to find documentation about components, parameters, functions, and blog posts.' + WHEN NOT EXISTS ( + SELECT 1 FROM documentation_fts + WHERE documentation_fts = $escaped_search + ) THEN 'No results found for "' || $search || '".' + ELSE NULL + END as contents; + +SELECT 'list' as component, + 'Search Results' as title, + 'No results found for "' || $search || '".' as empty_description +WHERE $search IS NOT NULL; + +WITH search_results AS ( + SELECT + COALESCE( + component_name || ' component: parameter ' || parameter_name + , component_name || ' component' || IF(component_example_description IS NULL, '', ' example') + , 'blog: ' || blog_title + , 'sqlpage.' || function_name || '(...' || function_parameter_name || '...)' + , 'sqlpage.' || function_name || '(...)' + ) as title, + COALESCE( + component_description, + parameter_description, + blog_description, + function_parameter_description, + function_description, + component_example_description + ) as description, + CASE + WHEN component_name IS NOT NULL THEN + json_object( + 'page', '/component.sql', + 'parameters', json_object('component', component_name) + ) + WHEN parameter_name IS NOT NULL THEN + json_object( + 'page', '/component.sql', + 'parameters', json_object('component', ( + SELECT component + FROM parameter + WHERE name = parameter_name + LIMIT 1 + )) + ) + WHEN blog_title IS NOT NULL THEN + json_object( + 'page', '/blog.sql', + 'parameters', json_object('post', blog_title) + ) + WHEN function_name IS NOT NULL THEN + json_object( + 'page', '/functions.sql', + 'parameters', json_object('function', function_name) + ) + WHEN function_parameter_name IS NOT NULL THEN + json_object( + 'page', '/functions.sql', + 'parameters', json_object('function', ( + SELECT function + FROM sqlpage_function_parameters + WHERE name = function_parameter_name + LIMIT 1 + )) + ) + END as link_data, + rank + FROM documentation_fts + WHERE $search IS NOT NULL + AND documentation_fts = $escaped_search +) +SELECT + max(title) as title, + max(description) as description, + sqlpage.link(link_data->>'page', link_data->'parameters') as link +FROM search_results +GROUP BY link_data +ORDER BY + rank, + CASE + WHEN title LIKE 'component:%' THEN 1 + WHEN title LIKE 'parameter:%' THEN 2 + WHEN title LIKE 'blog:%' THEN 3 + WHEN title LIKE 'function:%' THEN 4 + END, + description; diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index 68023e34..f562cb23 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -51,21 +51,30 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('active', 'Whether this item in the list is considered "active". Active items are displayed more prominently.', 'BOOLEAN', FALSE, TRUE), ('view_link', 'A URL to which the user should be taken when they click on the "view" icon. Does not show the icon when omitted.', 'URL', FALSE, TRUE), ('edit_link', 'A URL to which the user should be taken when they click on the "edit" icon. Does not show the icon when omitted.', 'URL', FALSE, TRUE), - ('delete_link', 'A URL to which the user should be taken when they click on the "delete" icon. Does not show the icon when omitted.', 'URL', FALSE, TRUE) + ('delete_link', 'A page that will be loaded when the user clicks on the delete button for this specific item. The link will be submitted as a POST request.', 'URL', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES ('list', 'A basic compact list', json('[{"component":"list", "compact": true, "title": "SQLPage lists are..."},{"title":"Beautiful"},{"title":"Useful"},{"title":"Versatile"}]')), ('list', 'An empty list with a link to add an item', json('[{"component":"list", "empty_title": "No items yet", "empty_description": "This list is empty. Click here to create a new item !", "empty_link": "documentation.sql"}]')), - ('list', 'A list with rich text descriptions', json('[{"component":"list", "wrap": true}, - {"title":"SQLPage", "image_url": "https://raw.githubusercontent.com/lovasoa/SQLpage/main/docs/favicon.png", "description_md":"A **SQL**-based **page** generator for **PostgreSQL**, **MySQL**, **SQLite** and **SQL Server**. [Free on Github](https://github.com/lovasoa/sqlpage)"}, - {"title":"Tabler", "image_url": "https://avatars.githubusercontent.com/u/35471246", "description_md":"A **free** and **open-source** **HTML** template pack based on **Bootstrap**."}, - {"title":"Tabler Icons", "image_url": "https://tabler.io/favicon.ico", "description_md":"A set of over **700** free MIT-licensed high-quality **SVG** icons for you to use in your web projects."} + ('list', ' +### A list with rich text descriptions + +This example illustrates creating a nice list where each item has a title, a description, an image, and a link to another page. + +> Be careful, nested links are not supported. If you use the list''s `link` property, then your markdown `description_md` should not contain any link. +', json('[{"component":"list", "wrap": true}, + {"title":"SQL Websites", "image_url": "/favicon.ico", "description_md":"Write SQL, get a website. SQLPage is a **SQL**-based **site** generator for **PostgreSQL**, **MySQL**, **SQLite** and **SQL Server**.", "link": "/"}, + {"title":"SQL Forms", "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/b6/FileStack_retouched.jpg", "description_md":"Easily collect data **from users to your database** using the *form* component. Handle the data in SQL with `INSERT` or `UPDATE` queries.", "link": "?component=form"}, + {"title":"SQL Maps", "image_url": "https://upload.wikimedia.org/wikipedia/commons/1/15/Vatican_City_map_EN.png", "description_md":"Show database contents on a map using the *map* component. Works well with *PostGIS* and *SpatiaLite*.", "link": "?component=map"}, + {"title":"Advanced features", "icon": "settings", "description_md":"[Authenticate users](?component=authentication), [edit data](?component=form), [generate an API](?component=json), [maintain your database schema](/your-first-sql-website/migrations.sql), and more."} ]')), ('list', 'A beautiful list with bells and whistles.', - json('[{"component":"list", "title":"Popular websites" }, '|| - '{"title":"Google", "link":"https://google.com", "description": "A search engine", "color": "red", "icon":"brand-google", "active": true }, '|| - '{"title":"Wikipedia", "link":"https://wikipedia.org", "description": "An encyclopedia", "color": "blue", "icon":"world", "edit_link": "?edit=wikipedia", "delete_link": "?delete=wikipedia" }]')); + json('[{"component":"list", "title":"Top SQLPage features", "compact": true }, + {"title":"Authentication", "link":"?component=authentication", "description": "Authenticate users with a login form or HTTP basic authentication", "color": "red", "icon":"lock", "active": true, "view_link": "?component=authentication#view" }, + {"title":"Editing data", "link":"?component=form", "description": "SQLPage makes it easy to UPDATE, INSERT and DELETE data in your database tables", "color": "blue", "icon":"database", "edit_link": "?component=form#edit", "delete_link": "?component=form#delete" }, + {"title":"API", "link":"?component=json", "description": "Generate a REST API from a single SQL query to connect with other applications and services", "color": "green", "icon":"plug-connected", "edit_link": "?component=json#edit", "delete_link": "?component=json#delete" } + ]')); INSERT INTO component(name, icon, description) VALUES ('datagrid', 'grid-dots', 'Display small pieces of information in a clear and readable way. Each item has a name and is associated with a value.'); @@ -124,8 +133,8 @@ INSERT INTO example(component, description, properties) VALUES ('steps', 'Online store checkout steps.', json('[{"component":"steps"},{"title":"Shopping"},{"title":"Store pickup"}, {"title":"Payment","active":true},{"title":"Review & Order"}]')), ('steps', 'A progress indicator with custom color, auto-generated step numbers, icons, and description tooltips.', json('[{"component":"steps", "counter": true, "color":"purple"}, '|| - '{"title": "Registration form", "icon":"forms", "link": "https://github.com/lovasoa/sqlpage", "description": "Initial account data creation."},' || - '{"title": "Email confirmation", "icon": "mail", "link": "https://sql.ophir.dev", "description": "Confirm your email by clicking on a link in a validation email."},' || + '{"title": "Registration form", "icon":"forms", "link": "https://github.com/sqlpage/SQLPage", "description": "Initial account data creation."},' || + '{"title": "Email confirmation", "icon": "mail", "link": "https://sql-page.com", "description": "Confirm your email by clicking on a link in a validation email."},' || '{"title": "ID verification", "description": "Checking personal information", "icon": "user", "link": "#"},' || '{"title": "Final account approval", "description": "ophir.dev", "link": "https://ophir.dev/", "icon":"eye-check", "active": true},' || '{"title":"Account creation", "icon":"check"}]')); @@ -141,6 +150,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('html', 'Raw html code to include on the page. Don''t use that if you are not sure what you are doing, it may have security implications.', 'TEXT', TRUE, TRUE), ('contents', 'A top-level paragraph of text to display, without any formatting, without having to make additional queries.', 'TEXT', TRUE, TRUE), ('contents_md', 'Rich text in the markdown format. Among others, this allows you to write bold text using **bold**, italics using *italics*, and links using [text](https://example.com).', 'TEXT', TRUE, TRUE), + ('article', 'Makes long texts more readable by increasing the line height, adding margins, using a serif font, and decorating the initial letter.', 'BOOLEAN', TRUE, TRUE), -- item level ('contents', 'A span of text to display', 'TEXT', FALSE, FALSE), ('contents_md', 'Rich text in the markdown format. Among others, this allows you to write bold text using **bold**, italics using *italics*, and links using [text](https://example.com).', 'TEXT', FALSE, TRUE), @@ -156,10 +166,10 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S INSERT INTO example(component, description, properties) VALUES ('text', 'Rendering a simple text paragraph.', json('[{"component":"text", "contents":"Hello, world ! <3"}]')), - ('text', 'Rendering rich text using markdown', json('[{"component":"text", "contents_md":"\n'|| + ('text', 'Rendering rich text using markdown', json('[{"component":"text", "article": true, "contents_md":"\n'|| '# Markdown in SQLPage\n\n' || '## Simple formatting\n\n' || - 'SQLPage supports only plain text as column values, but markdown allows easily adding **bold**, *italics*, [external links](https://github.com/lovasoa/sqlpage), [links to other pages](/index.sql) and [intra-page links](#my-paragraph). \n\n' || + 'SQLPage supports only plain text as column values, but markdown allows easily adding **bold**, *italics*, [external links](https://github.com/sqlpage/SQLPage), [links to other pages](/index.sql) and [intra-page links](#my-paragraph). \n\n' || '## Lists\n' || '### Unordered lists\n' || '* SQLPage is easy\n' || @@ -177,15 +187,15 @@ INSERT INTO example(component, description, properties) VALUES '## Tables\n\n' || '| SQLPage component | Description | Documentation link |\n' || '| --- | --- | --- |\n' || - '| text | A paragraph of text. | [Documentation](https://sql.ophir.dev/documentation.sql?component=text) |\n' || - '| list | A list of items. | [Documentation](https://sql.ophir.dev/documentation.sql?component=list) |\n' || - '| steps | A progress indicator. | [Documentation](https://sql.ophir.dev/documentation.sql?component=steps) |\n' || - '| form | A series of input fields. | [Documentation](https://sql.ophir.dev/documentation.sql?component=form) |\n\n' || + '| text | A paragraph of text. | [Documentation](https://sql-page.com/documentation.sql?component=text) |\n' || + '| list | A list of items. | [Documentation](https://sql-page.com/documentation.sql?component=list) |\n' || + '| steps | A progress indicator. | [Documentation](https://sql-page.com/documentation.sql?component=steps) |\n' || + '| form | A series of input fields. | [Documentation](https://sql-page.com/documentation.sql?component=form) |\n\n' || '## Quotes\n' || '> Fantastic.\n>\n' || '> — [HackerNews User](https://news.ycombinator.com/item?id=36194473#36209061) about SQLPage\n\n' || '## Images\n' || - '![SQLPage logo](https://sql.ophir.dev/favicon.ico)\n\n' || + '![SQLPage logo](https://sql-page.com/favicon.ico)\n\n' || '## Horizontal rules\n' || '---\n\n' || '"}]')), @@ -207,11 +217,28 @@ INSERT INTO example(component, description, properties) VALUES ; INSERT INTO component(name, icon, description) VALUES - ('form', 'cursor-text', 'A series of input fields that can be filled in by the user. ' || - 'The form contents can be posted and handled by another sql file in your site. ' || - 'The value entered by the user in a field named x will be accessible to the target SQL page as a variable named $x. - For instance, you can create a SQL page named "create_user.sql" that would contain "INSERT INTO users(name) VALUES($name)" - and a form with its action property set to "create_user.sql" that would contain a field named "name".'); + ('form', 'cursor-text', ' +# Building forms in SQL + +The form component will display a series of input fields of various types, that can be filled in by the user. +When the user submits the form, the data is posted to an SQL file specified in the `action` property. + +## Handle Data with SQL + +The receiving SQL page will be able to handle the data, +and insert it into the database, use it to perform a search, format it, update existing data, etc. + +A value in a field named "x" will be available as `:x` in the SQL query of the target page. + +## Examples + + - [A multi-step form](https://github.com/sqlpage/SQLPage/tree/main/examples/forms-with-multiple-steps), guiding the user through a process without overwhelming them with a large form. + - [File upload form](https://github.com/sqlpage/SQLPage/tree/main/examples/image%20gallery%20with%20user%20uploads), letting users upload images to a gallery. + - [Rich text editor](https://github.com/sqlpage/SQLPage/tree/main/examples/rich-text-editor), letting users write text with bold, italics, links, images, etc. + - [Master-detail form](https://github.com/sqlpage/SQLPage/tree/main/examples/master-detail-forms), to edit a list of structured items. + - [Form with a variable number of fields](https://github.com/sqlpage/SQLPage/tree/main/examples/forms%20with%20a%20variable%20number%20of%20fields), when the fields are not known in advance. + - [Demo of all input types](/examples/form), showing all the input types supported by SQLPage. +'); INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'form', * FROM (VALUES -- top level ('enctype', ' @@ -239,8 +266,9 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('validate_outline', 'A color to outline the validation button.', 'COLOR', TRUE, TRUE), ('reset', 'The text to display in the button at the bottom of the form that resets the form to its original state. Omit this property not to show a reset button at all.', 'TEXT', TRUE, TRUE), ('id', 'A unique identifier for the form, which can then be used to validate the form from a button outside of the form.', 'TEXT', TRUE, TRUE), + ('auto_submit', 'Automatically submit the form when the user changes any of its fields, and remove the validation button.', 'BOOLEAN', TRUE, TRUE), -- item level - ('type', 'The type of input to use: text for a simple text field, textarea for a multi-line text input control, number to accept only numbers, checkbox or radio for a button that is part of a group specified in the ''name'' parameter, hidden for a value that will be submitted but not shown to the user. text by default.', 'TEXT', FALSE, TRUE), + ('type', 'Declares input control behavior and expected format. All HTML input types are supported (text, number, date, file, checkbox, radio, hidden, ...). SQLPage adds some custom types: textarea, switch, header. text by default. See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#input_types', 'TEXT', FALSE, TRUE), ('name', 'The name of the input field, that you can use in the target page to get the value the user entered for the field.', 'TEXT', FALSE, FALSE), ('label', 'A friendly name for the text field to show to the user.', 'TEXT', FALSE, TRUE), ('placeholder', 'A placeholder text that will be shown in the field when is is empty.', 'TEXT', FALSE, TRUE), @@ -248,14 +276,16 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('options', 'A json array of objects containing the label and value of all possible options of a select field. Used only when type=select. JSON objects in the array can contain the properties "label", "value" and "selected".', 'JSON', FALSE, TRUE), ('required', 'Set this to true to prevent the form contents from being sent if this field is left empty by the user.', 'BOOLEAN', FALSE, TRUE), ('min', 'The minimum value to accept for an input of type number', 'REAL', FALSE, TRUE), - ('max', 'The minimum value to accept for an input of type number', 'REAL', FALSE, TRUE), + ('max', 'The maximum value to accept for an input of type number', 'REAL', FALSE, TRUE), ('checked', 'Used only for checkboxes and radio buttons. Indicates whether the checkbox should appear as already checked.', 'BOOLEAN', FALSE, TRUE), ('multiple', 'Used only for select elements. Indicates that multiple elements can be selected simultaneously. When using multiple, you should add square brackets after the variable name: ''my_variable[]'' as name', 'BOOLEAN', FALSE, TRUE), + ('empty_option', 'Only for inputs of type `select`. Adds an empty option with the given label before the ones defined in `options`. Useful when generating other options from a database table.', 'TEXT', FALSE, TRUE), ('searchable', 'For select and multiple-select elements, displays them with a nice dropdown that allows searching for options.', 'BOOLEAN', FALSE, TRUE), ('dropdown', 'An alias for "searchable".', 'BOOLEAN', FALSE, TRUE), ('create_new', 'In a multiselect with a dropdown, this option allows the user to enter new values, that are not in the list of options.', 'BOOLEAN', FALSE, TRUE), ('step', 'The increment of values in an input of type number. Set to 1 to allow only integers.', 'REAL', FALSE, TRUE), ('description', 'A helper text to display near the input field.', 'TEXT', FALSE, TRUE), + ('description_md', 'A helper text to display near the input field - formatted using markdown.', 'TEXT', FALSE, TRUE), ('pattern', 'A regular expression that the value must match. For instance, [0-9]{3} will only accept 3 digits.', 'TEXT', FALSE, TRUE), ('autofocus', 'Automatically focus the field when the page is loaded', 'BOOLEAN', FALSE, TRUE), ('width', 'Width of the form field, between 1 and 12.', 'INTEGER', FALSE, TRUE), @@ -268,6 +298,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('prefix','Text to display on the left side of the input field, on the same line.','TEXT',FALSE,TRUE), ('suffix','Short text to display after th input, on the same line. Useful to add units or a currency symbol to an input.','TEXT',FALSE,TRUE), ('readonly','Set to true to prevent the user from modifying the value of the input field.','BOOLEAN',FALSE,TRUE), + ('rows','Number of rows to display for a textarea. Defaults to 3.','INTEGER',FALSE,TRUE), ('disabled','Makes the field non-editable, non-focusable, and not submitted with the form. Use readonly instead for simple non-editable fields.','BOOLEAN',FALSE,TRUE), ('id','A unique identifier for the input, which can then be used to select and manage the field with Javascript code. Usefull for advanced using as setting client side event listeners, interactive control of input field (disabled, visibility, read only, e.g.) and AJAX requests.','TEXT',FALSE,TRUE) ) x; @@ -302,16 +333,34 @@ When loading the page, the value for `:username` will be `NULL` if no value has '{"name": "Last name", "required": true, "description": "We need your last name for legal purposes."},'|| '{"name": "Resume", "type": "textarea"},'|| '{"name": "Birth date", "type": "date", "max": "2010-01-01", "value": "1994-04-16"},'|| - '{"name": "Password", "type": "password", "pattern": "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", "required": true, "description": "Minimum eight characters, at least one letter and one number."},'|| + '{"name": "Password", "type": "password", "pattern": "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", "required": true, "description_md": "**Password Requirements:** Minimum **8 characters**, at least **one letter** & **one number**. *Tip:* Use a passphrase for better security!"},'|| '{"label": "I accept the terms and conditions", "name": "terms", "type": "checkbox", "required": true}'|| ']')), ('form','Create prepended and appended inputs to make your forms easier to use.', json('[{"component":"form"}, '|| '{"name": "Your account", "prefix_icon": "mail", "prefix": "Email:", "suffix": "@mydomain.com"}, ' || ']')), + + ('form','With the header type, you can group your input fields based on a theme. For example, you can categorize fields according to a person''s identity and their contact information.', + json('[{"component":"form","title":"Information about the person"}, '|| + '{"type": "header", "label": "Identity"},' || + '{"name": "Name"},' || + '{"name": "Surname"},' || + '{"type": "header","label": "Contact"},' || + '{"name": "phone", "label": "Phone number"},' || + '{"name": "Email"},' || + ']')), + + ('form','A toggle switch in an HTML form is a user interface element that allows users to switch between two states, typically "on" and "off." It visually resembles a physical switch and is often used for settings or options that can be enabled or disabled.', + json('[{"component":"form"}, + {"type": "switch", "label": "Dark theme", "name": "dark", "description": "Enable dark theme"}, + {"type": "switch", "label": "A required toggle switch", "name": "my_checkbox", "required": true,"checked": true}, + {"type": "switch", "label": "A disabled toggle switch", "name": "my_field", "disabled": true} + ]')), + ('form', 'This example illustrates the use of the `select` type. In this select input, the various options are hardcoded, but they could also be loaded from a database table, -using a function to convert the rows into a json array like +[using a function to convert the rows into a json array](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) like - `json_group_array()` in SQLite, - `json_agg()` in Postgres, - `JSON_ARRAYAGG()` in MySQL, or @@ -322,38 +371,49 @@ In SQLite, the query would look like ```sql SELECT ''select'' as type, + ''Select a fruit...'' as empty_option, json_group_array(json_object( - "label", name, - "value", id + ''label'', name, + ''value'', id )) as options FROM fruits ``` ', json('[{"component":"form", "action":"examples/show_variables.sql"}, - {"name": "Fruit", "type": "select", "searchable": true, "value": 1, "options": - "[{\"label\": \"Orange\", \"value\": 0}, {\"label\": \"Apple\", \"value\": 1}, {\"label\": \"Banana\", \"value\": 3}]"} + {"name": "Fruit", "type": "select", + "empty_option": "Select a fruit...", + "options": + "[{\"label\": \"Orange\", \"value\": 0}, {\"label\": \"Apple\", \"value\": 1}, {\"label\": \"Banana\", \"value\": 3}]"} ]')), ('form', '### Multi-select You can authorize the user to select multiple options by setting the `multiple` property to `true`. This creates a more compact (but arguably less user-friendly) alternative to a series of checkboxes. -In this case, you should add square brackets to the name of the field. +In this case, you should add square brackets to the name of the field (e.g. `''my_field[]'' as name`). The target page will then receive the value as a JSON array of strings, which you can iterate over using - the `json_each` function [in SQLite](https://www.sqlite.org/json1.html) and [Postgres](https://www.postgresql.org/docs/9.3/functions-json.html), - the [`OPENJSON`](https://learn.microsoft.com/fr-fr/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16) function in Microsoft SQL Server. - - in MySQL, json manipulation is less straightforward: see [the SQLPage MySQL json example](https://github.com/lovasoa/SQLpage/tree/main/examples/mysql%20json%20handling) + - in MySQL, json manipulation is less straightforward: see [the SQLPage MySQL json example](https://github.com/sqlpage/SQLPage/tree/main/examples/mysql%20json%20handling) + +[More information on how to handle JSON in SQL](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). The target page could then look like this: ```sql insert into best_fruits(id) -- INSERT INTO ... SELECT ... runs the SELECT query and inserts the results into the table select CAST(value AS integer) as id -- all values are transmitted by the browser as strings -from json_each($preferred_fruits); -- json_each returns a table with a "value" column for each element in the JSON array +from json_each($my_field); -- in SQLite, json_each returns a table with a "value" column for each element in the JSON array ``` ### Example multiselect generated from a database table -As an example, if you have a table of all possible options (`my_options(id int, label text)`), -and another table that contains the selected options per user (`my_user_options(user_id int, option_id int)`), -you can use a query like this to generate the multi-select field: +If you have a table of all possible options (`my_options(id int, label text)`), +and want to generate a multi-select field from it, you have two options: +- if the number of options is not too large, you can use the `options` parameter to return them all as a JSON array in the SQL query +- if the number of options is large (e.g. more than 1000), you can use `options_source` to load options dynamically from a different SQL query as the user types + +#### Embedding all options in the SQL query + +Let''s say you have a table that contains the selected options per user (`my_user_options(user_id int, option_id int)`). +You can use a query like this to generate the multi-select field: ```sql select ''select'' as type, true as multiple, json_group_array(json_object( @@ -366,10 +426,33 @@ left join my_user_options on my_options.id = my_user_options.option_id and my_user_options.user_id = $user_id ``` -', json('[{"component":"form", "action":"examples/show_variables.sql"}, - {"label": "Fruits", "name": "fruits[]", "type": "select", "multiple": true, "create_new":true, "placeholder": "Good fruits...", "searchable": true, "description": "press ctrl to select multiple values", "options": - "[{\"label\": \"Orange\", \"value\": 0, \"selected\": true}, {\"label\": \"Apple\", \"value\": 1}, {\"label\": \"Banana\", \"value\": 3, \"selected\": true}]"} - ]')), + +This will generate a json array of objects, each containing the label, value and selected status of each option. + +#### Loading options dynamically from a different SQL query with `options_source` + +If the `my_options` table has a large number of rows, you can use the `options_source` parameter to load options dynamically from a different SQL query as the user types. + +We''ll write a second SQL file, `options_source.sql`, that will receive the user''s search string as a parameter named `$search`, + and return a json array of objects, each containing the label and value of each option. + +##### `options_source.sql` + +```sql +select ''json'' as component; + +select id as value, label as label +from my_options +where label like $search || ''%''; +``` + +##### `form` + +', json('[{"component":"form", "action":"examples/show_variables.sql", "reset": "Reset"}, + {"name": "component", "type": "select", + "options_source": "examples/from_component_options_source.sql", + "description": "Start typing the name of a component like ''map'' or ''form''..." + }]')), ('form', 'This example illustrates the use of the `radio` type. The `name` parameter is used to group the radio buttons together. The `value` parameter is used to set the value that will be submitted when the user selects the radio button. @@ -380,16 +463,33 @@ We could also save all the options in a database table, and then run a simple qu ```sql SELECT ''form'' AS component; -SELECT * FROM fruit_option; +SELECT + ''radio'' as type, + ''db'' as name, + option_name as label, + option_id as value +FROM my_options; ``` -In this example, depending on what the user clicks, the target `index.sql` page will be loaded with a the variable `$fruit` set to the string "1", "2", or "3". +In this example, depending on what the user clicks, the page will be reloaded with a the variable `$component` set to the string "form", "map", or "chart". - ', json('[{"component":"form", "method": "GET", "action": "index.sql"}, '|| - '{"name": "fruit", "type": "radio", "value": 1, "description": "An apple a day keeps the doctor away", "label": "Apple"}, '|| - '{"name": "fruit", "type": "radio", "value": 2, "description": "Oranges are a good source of vitamin C", "label": "Orange", "checked": true}, '|| - '{"name": "fruit", "type": "radio", "value": 3, "description": "Bananas are a good source of potassium", "label": "Banana"}'|| - ']')), + ', json('[{"component":"form", "method": "GET"}, + {"name": "component", "type": "radio", "value": "form", "description": "Read user input in SQL", "label": "Form"}, + {"name": "component", "type": "radio", "value": "map", "checked": true, "description": "Display a map based on database data", "label": "Map"}, + {"name": "component", "type": "radio", "value": "chart", "description": "Interactive plots of SQL query results", "label": "Chart"} + ]')), + ('form', ' +### Dynamically refresh the page when the user changes the form + +The form will be automatically submitted when the user changes any of its fields, and the page will be reloaded with the new value. +The validation button is removed. +', json('[{"component":"form", "auto_submit": true}, + {"name": "component", "type": "select", "autocomplete": false, "options": [ + {"label": "Form", "value": "form", "selected": true}, + {"label": "Map", "value": "map"}, + {"label": "Chart", "value": "chart"} + ], "description": "Choose a component to view its documentation. No need to click a button, the page will be reloaded automatically.", "label": "Component"} + ]')), ('form', 'When you want to include some information in the form data, but not display it to the user, you can use a hidden field. This can be used to track simple data such as the current user''s id, @@ -540,7 +640,7 @@ INSERT INTO component(name, icon, description) VALUES INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'chart', * FROM (VALUES -- top level ('title', 'The name of the chart.', 'TEXT', TRUE, TRUE), - ('type', 'The type of chart: "line", "area", "bar", "column", "pie", "scatter", "bubble", or "heatmap".', 'TEXT', TRUE, FALSE), + ('type', 'The type of chart. One of: "line", "area", "bar", "column", "pie", "scatter", "bubble", "heatmap", "rangeBar"', 'TEXT', TRUE, FALSE), ('time', 'Whether the x-axis represents time. If set to true, the x values will be parsed and formatted as dates for the user.', 'BOOLEAN', TRUE, TRUE), ('ymin', 'The minimal value for the y-axis.', 'REAL', TRUE, TRUE), ('ymax', 'The maximum value for the y-axis.', 'REAL', TRUE, TRUE), @@ -571,7 +671,7 @@ INSERT INTO example(component, description, properties) VALUES "component": "chart", "title": "Quarterly Revenue", "type": "area", - "color": "indigo", + "color": "blue-lt", "marker": 5, "time": true }, @@ -587,7 +687,7 @@ INSERT INTO example(component, description, properties) VALUES {"label": "Yes", "value": 65}, {"label": "No", "value": 35}]')), ('chart', 'A basic bar chart', json('[ - {"component":"chart", "type": "bar", "title": "Quarterly Results", "horizontal": true}, + {"component":"chart", "type": "bar", "title": "Quarterly Results", "horizontal": true, "labels": true}, {"label": "Tom", "value": 35}, {"label": "Olive", "value": 15}]')), ('chart', 'A TreeMap Chart allows you to display hierarchical data in a nested layout. This is useful for visualizing the proportion of each part to the whole.', json('[ @@ -604,7 +704,9 @@ INSERT INTO example(component, description, properties) VALUES '{"series": "Marketing", "x": 2022, "value": 15}, '|| '{"series": "Human resources", "x": 2021, "value": 30}, '|| '{"series": "Human resources", "x": 2022, "value": 55}]')), - ('chart', 'A line chart with multiple series.', json('[{"component":"chart", "title": "Revenue", "ymin": 0}, + ('chart', 'A line chart with multiple series. One of the most common types of charts, often used to show trends over time. +Also demonstrates the use of the `toolbar` attribute to allow the user to download the graph as an image or the data as a CSV file.', + json('[{"component":"chart", "title": "Revenue", "ymin": 0, "toolbar": true}, {"series": "Chicago Store", "x": 2021, "value": 35}, {"series": "Chicago Store", "x": 2022, "value": 15}, {"series": "Chicago Store", "x": 2023, "value": 45}, @@ -638,11 +740,18 @@ where each series is represented as a line in the chart: The `color` property sets the color of each series separately, in order. ',json('[ {"component":"chart", "title": "Survey Results", "type": "heatmap", - "ytitle": "Database managemet system", "xtitle": "Year", "color": ["red","orange","yellow"]}, - { "series": "PostgreSQL", "x": "2000", "y": 48},{ "series": "SQLite", "x": "2000", "y": 14},{ "series": "MySQL", "x": "2000", "y": 78}, - { "series": "PostgreSQL", "x": "2010", "y": 65},{ "series": "SQLite", "x": "2010", "y": 22},{ "series": "MySQL", "x": "2010", "y": 83}, - { "series": "PostgreSQL", "x": "2020", "y": 73},{ "series": "SQLite", "x": "2020", "y": 28},{ "series": "MySQL", "x": "2020", "y": 87} + "ytitle": "Database managemet system", "xtitle": "Year", "color": ["purple","purple","purple"]}, + { "series": "PostgreSQL", "x": "2000", "y": 48},{ "series": "SQLite", "x": "2000", "y": 44},{ "series": "MySQL", "x": "2000", "y": 78}, + { "series": "PostgreSQL", "x": "2010", "y": 65},{ "series": "SQLite", "x": "2010", "y": 62},{ "series": "MySQL", "x": "2010", "y": 83}, + { "series": "PostgreSQL", "x": "2020", "y": 73},{ "series": "SQLite", "x": "2020", "y": 38},{ "series": "MySQL", "x": "2020", "y": 87} ]')), + ('chart', 'A timeline displaying events with a start and an end date', + json('[ + {"component":"chart", "title": "Project Timeline", "type": "rangeBar", "time": true, "color": ["teal", "cyan"], "labels": true, "xmin": "2021-12-28", "xmax": "2022-01-04" }, + {"series": "Phase 1", "label": "Operations", "value": ["2021-12-29", "2022-01-02"]}, + {"series": "Phase 2", "label": "Operations", "value": ["2022-01-03", "2022-01-04"]}, + {"series": "Yearly maintenance", "label": "Maintenance", "value": ["2022-01-01", "2022-01-03"]} + ]')), ('chart', ' ## Multiple charts on the same line @@ -655,8 +764,8 @@ to the path of the file you want to include, followed by `?_sqlpage_embed`. ', json('[ {"component":"card", "title":"A dashboard with multiple graphs on the same line", "columns": 2}, - {"embed": "/examples/chart.sql?color=green&n=42&_sqlpage_embed", "footer_md": "You can find the sql file that generates the chart [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/chart.sql)" }, - {"embed": "/examples/chart.sql?_sqlpage_embed" }, + {"embed": "/examples/chart.sql?color=green&n=42&_sqlpage_embed"}, + {"embed": "/examples/chart.sql?_sqlpage_embed" } ]')); INSERT INTO component(name, icon, description) VALUES @@ -671,79 +780,385 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S -- top level ('sort', 'Make the columns clickable to let the user sort by the value contained in the column.', 'BOOLEAN', TRUE, TRUE), ('search', 'Add a search bar at the top of the table, letting users easily filter table rows by value.', 'BOOLEAN', TRUE, TRUE), + ('initial_search_value', 'Pre-fills the search bar used to filter the table. The user will still be able to edit the value to display table rows that will initially be filtered out.', 'TEXT', TRUE, TRUE), + ('search_placeholder', 'Customizes the placeholder text shown in the search input field. Replaces the default "Search..." with text that better describes what users should search for.', 'TEXT', TRUE, TRUE), ('markdown', 'Set this to the name of a column whose content should be interpreted as markdown . Used to display rich text with links in the table. This argument can be repeated multiple times to intepret multiple columns as markdown.', 'TEXT', TRUE, TRUE), ('icon', 'Set this to the name of a column whose content should be interpreted as a tabler icon name. Used to display icons in the table. This argument can be repeated multiple times to intepret multiple columns as icons. Introduced in v0.8.0.', 'TEXT', TRUE, TRUE), ('align_right', 'Name of a column the contents of which should be right-aligned. This argument can be repeated multiple times to align multiple columns to the right. Introduced in v0.15.0.', 'TEXT', TRUE, TRUE), + ('align_center', 'Name of a column the contents of which should be center-aligned. This argument can be repeated multiple times to align multiple columns to the center.', 'TEXT', TRUE, TRUE), + ('monospace', 'Name of a column the contents of which should be displayed in monospace. This argument can be repeated multiple times to display multiple columns in monospace. Introduced in v0.32.1.', 'TEXT', TRUE, TRUE), ('striped_rows', 'Whether to add zebra-striping to any table row.', 'BOOLEAN', TRUE, TRUE), ('striped_columns', 'Whether to add zebra-striping to any table column.', 'BOOLEAN', TRUE, TRUE), ('hover', 'Whether to enable a hover state on table rows.', 'BOOLEAN', TRUE, TRUE), ('border', 'Whether to draw borders on all sides of the table and cells.', 'BOOLEAN', TRUE, TRUE), ('overflow', 'Whether to to let "wide" tables overflow across the right border and enable browser-based horizontal scrolling.', 'BOOLEAN', TRUE, TRUE), ('small', 'Whether to use compact table.', 'BOOLEAN', TRUE, TRUE), - ('description','Description of the table content and helps users with screen readers to find a table and understand what it’s.','TEXT',TRUE,TRUE), + ('description','Description of the table contents. Helps users with screen readers to find a table and understand what it’s about.','TEXT',TRUE,TRUE), ('empty_description', 'Text to display if the table does not contain any row. Defaults to "no data".', 'TEXT', TRUE, TRUE), + ('freeze_columns', 'Whether to freeze the leftmost column of the table.', 'BOOLEAN', TRUE, TRUE), + ('freeze_headers', 'Whether to freeze the top row of the table.', 'BOOLEAN', TRUE, TRUE), + ('freeze_footers', 'Whether to freeze the footer (bottom row) of the table, only works if that row has the `_sqlpage_footer` property applied to it.', 'BOOLEAN', TRUE, TRUE), + ('raw_numbers', 'Name of a column whose values are numeric, but should be displayed as raw numbers without any formatting (no thousands separators, decimal separator is always a dot). This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE), + ('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE), + ('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE), + ('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE), + ('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', TRUE, TRUE), -- row level ('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE), - ('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'TEXT', FALSE, TRUE) + ('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE), + ('_sqlpage_footer', 'Sets this row as the table footer. It is recommended that this parameter is applied to the last row. Added in v0.34.0.', 'BOOLEAN', FALSE, TRUE), + ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE), + ('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES ('table', 'The most basic table.', json('[{"component":"table"}, {"a": 1, "b": 2}, {"a": 3, "b": 4}]')), ('table', 'A table of users with filtering and sorting.', - json('[{"component":"table", "sort":true, "search":true}, '|| - '{"Forename": "Ophir", "Surname": "Lojkine", "Pseudonym": "lovasoa"},' || - '{"Forename": "Linus", "Surname": "Torvalds", "Pseudonym": "torvalds"}]')), + json('[ + {"component":"table", "sort":true, "search":true, "search_placeholder": "Filter by name"}, + {"First Name": "Ophir", "Last Name": "Lojkine", "Pseudonym": "lovasoa"}, + {"First Name": "Linus", "Last Name": "Torvalds", "Pseudonym": "torvalds"} + ]')), ('table', 'A table that uses markdown to display links', - json('[{"component":"table", "markdown": "Documentation", "icon": "icon", "sort": true, "search": true}, '|| - '{"icon": "table", "name": "Table", "description": "Displays SQL results as a searchable table.", "Documentation": "[docs](documentation.sql?component=table)", "_sqlpage_color": "red"}, - {"icon": "timeline", "name": "Chart", "description": "Show graphs based on numeric data.", "Documentation": "[docs](documentation.sql?component=chart)"} + json('[{"component":"table", "markdown": "Name", "icon": "icon", "search": true}, '|| + '{"icon": "table", "name": "[Table](?component=table)", "description": "Displays SQL results as a searchable table.", "_sqlpage_color": "red"}, + {"icon": "timeline", "name": "[Chart](?component=chart)", "description": "Show graphs based on numeric data."} ]')), + ('table', 'A sortable table with a colored footer showing the average value of its entries.', + json('[{"component":"table", "sort":true}, '|| + '{"Person": "Rudolph Lingens", "Height": 190},' || + '{"Person": "Jane Doe", "Height": 150},' || + '{"Person": "John Doe", "Height": 200},' || + '{"_sqlpage_footer":true, "_sqlpage_color": "green", "Person": "Average", "Height": 180}]')), ( 'table', - 'A table with numbers', + 'A table with column sorting. Sorting sorts numbers in numeric order, and strings in alphabetical order. + +Numbers can be displayed + - as raw digits without formatting using the `raw_numbers` property, + - as currency using the `money` property to define columns that contain monetary values and `currency` to define the currency, + - as numbers with a fixed maximum number of decimal digits using the `number_format_digits` property. +', json( - '[{"component":"table", "search": true, "sort": true, "align_right": ["Price ($)", "Amount in stock"]}, ' || - '{"id": 31456, "part_no": "MIC-ROCC-F-23-206-C", "Price ($)": 12, "Amount in stock": 5}, - {"id": 996, "part_no": "MIC-ROCC-F-24-206-A", "Price ($)": 1, "Amount in stock": 15}, - {"id": 131456, "part_no": "KIB-ROCC-F-13-205-B", "Price ($)": 127, "Amount in stock": 9} + '[{"component":"table", "sort": true, "align_right": ["Price", "Amount in stock"], "align_center": ["part_no"], "raw_numbers": ["id"], "currency": "USD", "money": ["Price"] }, + {"id": 31456, "part_no": "SQL-TABLE-856-G", "Price": 12, "Amount in stock": 5}, + {"id": 996, "part_no": "SQL-FORMS-86-M", "Price": 1, "Amount in stock": 1234}, + {"id": 131456, "part_no": "SQL-CARDS-56-K", "Price": 127, "Amount in stock": 98} ]' )), ( 'table', 'A table with some presentation options', json( - '[{"component":"table", "hover": true, "striped_rows": true, "description": "Some Star Trek Starfleet starships", "small": true},'|| - '{"name": "USS Enterprise", "registry": "NCC-1701-C", "class":"Ambassador"}, + '[{"component":"table", + "hover": true, "striped_rows": true, + "description": "Some Star Trek Starfleet starships", + "small": true, "initial_search_value": "NCC-" + }, + {"name": "USS Enterprise", "registry": "NCC-1701-C", "class":"Ambassador"}, {"name": "USS Archer", "registry": "NCC-44278", "class":"Archer"}, {"name": "USS Endeavour", "registry": "NCC-06", "class":"Columbia"}, {"name": "USS Constellation", "registry": "NCC-1974", "class":"Constellation"}, - {"name": "USS Dakota", "registry": "NCC-63892", "class":"Akira"} + {"name": "USS Dakota", "registry": "NCC-63892", "class":"Akira"}, + {"name": "USS Defiant", "registry": "IX-74205", "class":"Defiant"} ]' )), ( 'table', 'An empty table with a friendly message', json('[{"component":"table", "empty_description": "Nothing to see here at the moment."}]') - ); + ), + ( + 'table', + 'A large table with many rows and columns, with frozen columns on the left and headers on top. This allows users to browse large datasets without loosing track of their position.', + json('[ + {"component": "table", "freeze_columns": true, "freeze_headers": true}, + { + "feature": "SQL Execution", + "description": "Fully compatible with existing databases SQL dialects, executes any SQL query.", + "benefits": "Short learning curve, easy to use, interoperable with existing tools." + }, + { + "feature": "Data Visualization", + "description": "Automatic visualizations of query results: graphs, plots, pie charts, heatmaps, etc.", + "benefits": "Quickly analyze data trends, attractive and easy to understand, no external visualization tools or languages to learn." + }, + { + "feature": "User Authentication", + "description": "Supports user sessions, from basic auth to single sign-on.", + "benefits": "Secure, enforces access control policies, and provides a customizable security layer." + }, + { + "feature": "APIs", + "description": "Allows building JSON REST APIs and integrating with external APIs.", + "benefits": "Enables automation and integration with other platforms, facilitates data exchange." + }, + { + "feature": "Files", + "description": "File uploads, downloads and processing. Supports local filesystem and database storage.", + "benefits": "Convenient file management, secure data handling, flexible storage options, integrates with existing systems." + }, + { + "feature": "Maps", + "description": "Supports GeoJSON and is compatible with GIS data for map visualization.", + "benefits": "Geospatial data representation, integrates with geographic information systems." + }, + { + "feature": "Custom Components", + "description": "Build advanced features using HTML, JavaScript, and CSS.", + "benefits": "Tailor-made user experiences, easy to implement custom UI requirements." + }, + { + "feature": "Forms", + "description": "Insert and update data in databases based on user input.", + "benefits": "Simplified data input and management, efficient user interactions with databases." + }, + { + "feature": "DB Compatibility", + "description": "Works with MySQL, PostgreSQL, SQLite, Microsoft SQL Server and compatible databases.", + "benefits": "Broad compatibility with popular database systems, ensures seamless integration." + }, + { + "feature": "Security", + "description": "Built-in protection against common web vulnerabilities: no SQL injection, no XSS.", + "benefits": "Passes audits and security reviews, reduces the risk of data breaches." + }, + { + "feature": "Performance", + "description": "Designed for performance, with a focus on efficient data processing and minimal overhead.", + "benefits": "Quickly processes large datasets, handles high volumes of requests, and minimizes server load." + }, + { + "_sqlpage_footer": true, + "feature": "Summary", + "description": "Summarizes the features of the product.", + "benefits": "Provides a quick overview of the product''s features and benefits." + } +]') + ), + ( + 'table', + '# Dynamic column names in a table + +In all the previous examples, the column names were hardcoded in the SQL query. +This makes it very easy to quickly visualize the results of a query as a table, +but it can be limiting if you want to include columns that are not known in advance. +In situations when the number and names of the columns depend on the data, or on variables, +you can use the `dynamic` component to generate the table columns dynamically. + +For that, you will need to return JSON objects from your SQL query, where the keys are the column names, +and the values are the cell contents. + +Databases [offer utilities to generate JSON objects from query results](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) + - In PostgreSQL, you can use the [`json_build_object`](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-PROCESSING) +function for a fixed number of columns, or [`json_object_agg`](https://www.postgresql.org/docs/current/functions-aggregate.html#FUNCTIONS-AGGREGATE) for a dynamic number of columns. + - In SQLite, you can use the [`json_object`](https://www.sqlite.org/json1.html) function for a fixed number of columns, +or the `json_group_object` function for a dynamic number of columns. + - In MySQL, you can use the [`JSON_OBJECT`](https://dev.mysql.com/doc/refman/8.0/en/json-creation-functions.html#function_json-object) function for a fixed number of columns, +or the [`JSON_OBJECTAGG`](https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html#function_json-objectagg) function for a dynamic number of columns. + - In Microsoft SQL Server, you can use the [`FOR JSON PATH`](https://docs.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server?view=sql-server-ver15) clause. + +For instance, let''s say we have a table with three columns: store, item, and quantity_sold. +We want to create a pivot table where each row is a store, and each column is an item. +We will return a set of json objects that look like this: `{"store":"Madrid", "Item1": 42, "Item2": 7, "Item3": 0}` + +```sql +SELECT ''table'' AS component; +with filled_data as ( + select + stores.store, items.item, + (select coalesce(sum(quantity_sold), 0) from store_sales where store=stores.store and item=items.item) as quantity + from (select distinct store from store_sales) as stores + cross join (select distinct item from store_sales) as items + order by stores.store, items.item +) +SELECT + ''dynamic'' AS component, + JSON_PATCH( -- SQLite-specific, refer to your database documentation for the equivalent JSON functions + JSON_OBJECT(''store'', store), + JSON_GROUP_OBJECT(item, quantity) + ) AS properties +FROM + filled_data +GROUP BY + store; +``` + +This will generate a table with the stores in the first column, and the items in the following columns, with the quantity sold in each store for each item. + +', NULL + ), + ( + 'table', +'## Using Action Buttons in a table. + +### Preset Actions: `edit_url` & `delete_url` +Since edit and delete are common actions, the `table` component has dedicated `edit_url` and `delete_url` properties to add buttons for these actions. +The value of these properties should be a URL, containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. + +### Column with fixed action buttons + +You may want to add custom action buttons to your table rows, for instance to view details, download a file, or perform a custom operation. +For this, the `table` component has a `custom_actions` top-level property that lets you define a column of buttons, each button defined by a name, an icon, a link, and an optional tooltip. + +### Column with variable action buttons + +The `table` component also supports the row level `_sqlpage_actions` column in your data table. +This is helpful if you want a more complex logic, for instance to disable a button on some rows, or to change the link or icon based on the row data. + +> WARNING! +> If the number of array items in `_sqlpage_actions` is not consistent across all rows, the table may not render correctly. +> You can leave blank spaces by including an object with only the `name` property. + +The table has a column of buttons, each button defined by the `custom_actions` column at the table level, and by the `_sqlpage_actions` property at the row level. + +### `custom_actions` & `_sqlpage_actions` JSON properties. + +Each button is defined by the following properties: +* `name`: sets the column header and the tooltip if no tooltip is provided, +* `tooltip`: text to display when hovering over the button, +* `link`: the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row, +* `icon`: the tabler icon name or image link to display on the button + +### Example using all of the above +' + , + json('[ + { + "component": "table", + "edit_url": "/examples/show_variables.sql?action=edit&update_id={id}", + "delete_url": "/examples/show_variables.sql?action=delete&delete_id={id}", + "custom_actions": { + "name": "history", + "tooltip": "View Standard History", + "link": "/examples/show_variables.sql?action=history&standard_id={id}", + "icon": "history" + } + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "T23523", + "status": "Available", + "expires on": "2026-10-13", + "_sqlpage_id": 32, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action", + "tooltip": "Set In Use", + "link": "/examples/show_variables.sql?action=set_in_use&standard_id=32", + "icon": "caret-right" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "T2352", + "status": "In Use", + "expires on": "2026-10-14", + "_sqlpage_id": 33, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action", + "tooltip": "Retire Standard", + "link": "/examples/show_variables.sql?action=retire&standard_id=33", + "icon": "test-pipe-off" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "A123", + "status": "Discarded", + "expires on": "2026-09-30", + "_sqlpage_id": 31, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action" + } + ] + } +]' +) +); + INSERT INTO component(name, icon, description) VALUES - ('csv', 'download', 'A button that lets the user download data as a CSV file. Each column from the items in the component will map to a column in the resulting CSV.'); + ('csv', 'download', 'Lets the user download data as a CSV file. +Each column from the items in the component will map to a column in the resulting CSV. + +When `csv` is used as a **header component** (without a [shell](?component=shell)), it will trigger a download of the CSV file directly on page load. +If the csv file to download is large, we recommend using this approach. + +When used inside a page (after calling the shell component), this will add a button to the page that lets the user download the CSV file. +The button will need to load the entire contents of the CSV file in memory, inside the browser, even if the user does not click on it. +If the csv file to download is large, we recommend using this component without a shell component in order to efficiently stream the data to the browser. +'); INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'csv', * FROM (VALUES -- top level ('separator', 'How individual values should be separated in the CSV. "," by default, set it to "\t" for tab-separated values.', 'TEXT', TRUE, TRUE), ('title', 'The text displayed on the download button.', 'TEXT', TRUE, FALSE), ('filename', 'The name of the file that should be downloaded (without the extension).', 'TEXT', TRUE, TRUE), - ('icon', 'Name of the icon (from tabler-icons.io) to display in the button.', 'ICON', TRUE, TRUE), - ('color', 'Color of the button', 'COLOR', TRUE, TRUE), - ('size', 'The size of the button (e.g., sm, lg).', 'TEXT', TRUE, TRUE), + ('icon', 'Name of the icon (from tabler-icons.io) to display in the button. Ignored when used as a header component.', 'ICON', TRUE, TRUE), + ('color', 'Color of the button. Ignored when used as a header component.', 'COLOR', TRUE, TRUE), + ('size', 'The size of the button (e.g., sm, lg). Ignored when used as a header component.', 'TEXT', TRUE, TRUE), ('bom', 'Whether to include a Byte Order Mark (a special character indicating the character encoding) at the beginning of the file. This is useful for Excel compatibility.', 'BOOLEAN', TRUE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES - ('csv', 'CSV download button', + ('csv', ' +### Header component: creating a CSV download URL + +You can create a page that will trigger a download of the CSV file when the user visits it. +The contents will be streamed efficiently from the database to the browser, without being fully loaded in memory. +This makes it possible to download even very large files without overloading the database server, the web server, or the client''s browser. + +#### `csv_download.sql` + +```sql +select ''csv'' as component, ''example.csv'' as filename; +SELECT * FROM my_large_table; +``` + +#### `index.sql` +', + json('[{"component":"button"}, {"title": "Download my data", "link": "/examples/csv_download.sql"}]')), + ('csv', ' +### CSV download button + +This will generate a button to download the CSV file. +The button element itself will embed the entire contents of the CSV file, so it should not be used for large files. +The file will be entirely loaded in memory on the user''s browser, even if the user does not click on the button. +For smaller files, this is easier and faster to use than creating a separate SQL file to generate the CSV. +', json('[{"component":"csv", "title": "Download my data", "filename": "people", "icon": "file-download", "color": "green", "separator": ";", "bom": true}, '|| '{"Forename": "Ophir", "Surname": "Lojkine", "Pseudonym": "lovasoa"},' || '{"Forename": "Linus", "Surname": "Torvalds", "Pseudonym": "torvalds"}]')); @@ -771,7 +1186,8 @@ You can also store the data for a component in a `.json` file, and load it using This is particularly useful to create a single [shell](?component=shell#component) defining the site''s overall appearance and menus, and displaying it on all pages without duplicating its code. -The following will load the data for a `shell` component from a file named `shell.json` : +The following will load the data for a `shell` component from a file named `shell.json`, +using the [`sqlpage.read_file_as_text`](/functions.sql?function=read_file_as_text) function. ```sql SELECT ''dynamic'' AS component, sqlpage.read_file_as_text(''shell.json'') AS properties; @@ -788,13 +1204,38 @@ and `shell.json` would be placed at the website''s root and contain the followin {"link": "index.sql", "title": "Home"}, {"title": "Community", "submenu": [ {"link": "blog.sql", "title": "Blog"}, - {"link": "https//github.com/lovasoa/sqlpage/issues", "title": "Issues"}, - {"link": "https//github.com/lovasoa/sqlpage/discussions", "title": "Discussions"}, - {"link": "https//github.com/lovasoa/sqlpage", "title": "Github"} + {"link": "https//github.com/sqlpage/SQLPage/issues", "title": "Issues"}, + {"link": "https//github.com/sqlpage/SQLPage/discussions", "title": "Discussions"}, + {"link": "https//github.com/sqlpage/SQLPage", "title": "Github"} ]} ] } ``` +', NULL), +('dynamic', ' +## Including another SQL file + +To avoid repeating the same code on multiple pages, you can include another SQL file using the `dynamic` component +together with the [`sqlpage.run_sql`](/functions.sql?function=run_sql) function. + +For instance, the following will include the file `shell.sql` at the top of the page, +and pass it a `$title` variable to display the page title. + +```sql +SELECT ''dynamic'' AS component, + sqlpage.run_sql(''shell.sql'', json_object(''title'', ''SQLPage documentation'')) AS properties; +``` + +And `shell.sql` could contain the following: + +```sql +SELECT ''shell'' AS component, + COALESCE($title, ''Default title'') AS title, + ''/my_icon.png'' AS icon, + ''products'' AS menu_item, + ''about'' AS menu_item; +``` + ', NULL), ('dynamic', ' ## Dynamic shell @@ -815,9 +1256,9 @@ SELECT ''dynamic'' AS component, '' {"link": "index.sql", "title": "Home"}, {"title": "Community", "submenu": [ {"link": "blog.sql", "title": "Blog"}, - {"link": "https//github.com/lovasoa/sqlpage/issues", "title": "Issues"}, - {"link": "https//github.com/lovasoa/sqlpage/discussions", "title": "Discussions"}, - {"link": "https//github.com/lovasoa/sqlpage", "title": "Github"} + {"link": "https//github.com/sqlpage/SQLPage/issues", "title": "Issues"}, + {"link": "https//github.com/sqlpage/SQLPage/discussions", "title": "Discussions"}, + {"link": "https//github.com/sqlpage/SQLPage", "title": "Github"} ]} ] } @@ -826,13 +1267,46 @@ SELECT ''dynamic'' AS component, '' [View the result of this query, as well as an example of how to generate a dynamic menu based on the database contents](./examples/dynamic_shell.sql). +', NULL), + ('dynamic', ' +## Dynamic tables + +The `dynamic` component can be used to generate [tables](?component=table#component) with dynamic columns, +using [your database''s JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). + +For instance, let''s say we have a table with three columns: user_id, name, and role. +We want to create a table where each row is a user, and each column is a role. +We will return a set of json objects that look like this: `{"name": "Alice", "admin": true, "editor": false, "viewer": true}` +```sql +SELECT ''table'' AS component; +SELECT ''dynamic'' AS component, + json_patch( + json_object(''name'', name), + json_object_agg(role, is_admin) + ) AS properties +FROM users +GROUP BY name; +``` ', NULL); INSERT INTO component(name, icon, description) VALUES - ('shell', 'layout-navbar', 'Personalize the "shell" surrounding your page contents. Used to set properties for the entire page.'); + ('shell', 'layout-navbar', ' +Customize the overall layout, header and footer of the page. + +This is a special component that provides the page structure wrapping all other components on your page. + +It generates the complete HTML document including the `` section with metadata, title, and stylesheets, +as well as the navigation bar, main content area, and footer. + +If you don''t explicitly call the shell component at the top of your SQL file, SQLPage will automatically +add a default shell component before your first try to display data on the page. + +Use the shell component to customize page-wide settings like the page title, navigation menu, theme, fonts, +and to include custom visual styles (with CSS) or interactive behavior (with JavaScript) that should be loaded on the page. +'); INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'shell', * FROM (VALUES - ('favicon', 'The URL of the icon the web browser should display in bookmarks and tabs. This property is particularly useful if multiple sites are hosted on the same domain with different [``site_prefix``](https://github.com/lovasoa/SQLpage/blob/main/configuration.md#configuring-sqlpage).', 'URL', TRUE, TRUE), + ('favicon', 'The URL of the icon the web browser should display in bookmarks and tabs. This property is particularly useful if multiple sites are hosted on the same domain with different [``site_prefix``](https://github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage).', 'URL', TRUE, TRUE), ('manifest', 'The location of the [manifest.json](https://developer.mozilla.org/en-US/docs/Web/Manifest) if the site is a [PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps). Among other features, serving a manifest enables your site to be "installed" as an app on most mobile devices.', 'URL', TRUE, TRUE) ) x; INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'shell', * FROM (VALUES @@ -852,14 +1326,21 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('fixed_top_menu', 'Fixes the top bar with menu at the top (the top bar remains visible when scrolling long pages).', 'BOOLEAN', TRUE, TRUE), ('search_target', 'When this is set, a search field will appear in the top navigation bar, and load the specified sql file with an URL parameter named "search" when the user searches something.', 'TEXT', TRUE, TRUE), ('search_value', 'This value will be placed in the search field when "search_target" is set. Using the "$search" query parameter value will mirror the value that the user has searched for.', 'TEXT', TRUE, TRUE), + ('search_placeholder', 'Customizes the placeholder text shown in the search input field. Replaces the default "Search" with text that better describes what users should search for.', 'TEXT', TRUE, TRUE), + ('search_button', 'Customizes the text displayed on the search button. Replaces the default "Search" label with custom text that may better match your applications terminology or language.', 'TEXT', TRUE, TRUE), ('norobot', 'Forbids robots to save this page in their database and follow the links on this page. This will prevent this page to appear in Google search results for any query, for instance.', 'BOOLEAN', TRUE, TRUE), ('font', 'Specifies the font to be used for displaying text, which can be a valid font name from fonts.google.com or the path to a local WOFF2 font file starting with a slash (e.g., "/fonts/MyLocalFont.woff2").', 'TEXT', TRUE, TRUE), ('font_size', 'Font size on the page, in pixels. Set to 18 by default.', 'INTEGER', TRUE, TRUE), ('language', 'The language of the page. This can be used by search engines and screen readers to determine in which language the page is written.', 'TEXT', TRUE, TRUE), + ('rtl', 'Whether the page should be displayed in right-to-left mode. Used to display Arabic, Hebrew, Persian, etc.', 'BOOLEAN', TRUE, TRUE), ('refresh', 'Number of seconds after which the page should refresh. This can be useful to display dynamic content that updates automatically.', 'INTEGER', TRUE, TRUE), ('sidebar', 'Whether the menu defined by menu_item should be displayed on the left side of the page instead of the top. Introduced in v0.27.', 'BOOLEAN', TRUE, TRUE), + ('sidebar_theme', 'Used with sidebar property, It can be set to "dark" to exclusively set the sidebar into dark theme.', 'BOOLEAN', TRUE, TRUE), ('theme', 'Set to "dark" to use a dark theme.', 'TEXT', TRUE, TRUE), - ('footer', 'Muted text to display in the footer of the page. This can be used to display a link to the terms and conditions of your application, for instance. By default, shows "Built with SQLPage". Supports links with markdown.', 'TEXT', TRUE, TRUE) + ('footer', 'Muted text to display in the footer of the page. This can be used to display a link to the terms and conditions of your application, for instance. By default, shows "Built with SQLPage". Supports links with markdown.', 'TEXT', TRUE, TRUE), + ('preview_image', 'The URL of an image to display as a link preview when the page is shared on social media', 'URL', TRUE, TRUE), + ('navbar_title', 'The title to display in the top navigation bar. Used to display a different title in the top menu than the one that appears in the tab of the browser.', 'TEXT', TRUE, TRUE), + ('target', '"_blank" to open the link in a new tab, "_self" to open it in the same tab, "_parent" to open it in the parent frame, or "_top" to open it in the full body of the window', 'TEXT', TRUE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES @@ -872,57 +1353,78 @@ and in its object form, to generate a dropdown menu named "Community" with links The object form can be used directly only on database engines that have a native JSON type. On other engines (such as SQLite), you can use the [`dynamic`](?component=dynamic#component) component to generate the same result. + You see the [page layouts demo](./examples/layouts.sql) for a live example of the different layouts. ', json('[{ "component": "shell", - "title": "SQLPage", + "title": "SQLPage: SQL websites", "icon": "database", "link": "/", "menu_item": [ {"title": "About", "submenu": [ {"link": "/safety.sql", "title": "Security", "icon": "lock"}, {"link": "/performance.sql", "title": "Performance", "icon": "bolt"}, - {"link": "//github.com/lovasoa/SQLpage/blob/main/LICENSE.txt", "title": "License", "icon": "file-text"}, + {"link": "//github.com/sqlpage/SQLPage/blob/main/LICENSE.txt", "title": "License", "icon": "file-text"}, {"link": "/blog.sql", "title": "Articles", "icon": "book"} ]}, {"title": "Examples", "submenu": [ - {"link": "/examples/tabs.sql", "title": "Tabs", "icon": "layout-navbar"}, + {"link": "/examples/tabs/", "title": "Tabs", "icon": "layout-navbar"}, {"link": "/examples/layouts.sql", "title": "Layouts", "icon": "layout"}, {"link": "/examples/multistep-form", "title": "Forms", "icon": "edit"}, {"link": "/examples/handle_picture_upload.sql", "title": "File uploads", "icon": "upload"}, {"link": "/examples/authentication/", "title": "Password protection", "icon": "password-user"}, - {"link": "//github.com/lovasoa/SQLpage/blob/main/examples/", "title": "All examples & demos", "icon": "code"} + {"link": "//github.com/sqlpage/SQLPage/blob/main/examples/", "title": "All examples & demos", "icon": "code"} ]}, {"title": "Community", "submenu": [ - {"link": "blog.sql", "title": "Blog", "icon": "book"}, - {"link": "//github.com/lovasoa/sqlpage/issues", "title": "Report a bug", "icon": "bug"}, - {"link": "//github.com/lovasoa/sqlpage/discussions", "title": "Discussions", "icon": "message"}, - {"link": "//github.com/lovasoa/sqlpage", "title": "Github", "icon": "brand-github"} + {"link": "/blog.sql", "title": "Blog", "icon": "book"}, + {"link": "//github.com/sqlpage/SQLPage/issues", "title": "Report a bug", "icon": "bug"}, + {"link": "//github.com/sqlpage/SQLPage/discussions", "title": "Discussions", "icon": "message"}, + {"link": "//github.com/sqlpage/SQLPage", "title": "Github", "icon": "brand-github"} ]}, {"title": "Documentation", "submenu": [ {"link": "/your-first-sql-website", "title": "Getting started", "icon": "book"}, {"link": "/components.sql", "title": "All Components", "icon": "list-details"}, {"link": "/functions.sql", "title": "SQLPage Functions", "icon": "math-function"}, + {"link": "/extensions-to-sql", "title": "Extensions to SQL", "icon": "cube-plus"}, {"link": "/custom_components.sql", "title": "Custom Components", "icon": "puzzle"}, - {"link": "//github.com/lovasoa/SQLpage/blob/main/configuration.md#configuring-sqlpage", "title": "Configuration", "icon": "settings"} - ]} + {"link": "//github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage", "title": "Configuration", "icon": "settings"} + ]}, + {"title": "Search", "link": "/search"} ], "layout": "boxed", "language": "en-US", - "description": "Documentation for the SQLPage low-code web application framework.", + "description": "Go from SQL queries to web applications in an instant.", + "preview_image": "https://sql-page.com/sqlpage_social_preview.webp", + "theme": "dark", "font": "Poppins", "javascript": [ "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js", "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/sql.min.js", "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/handlebars.min.js", "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/languages/json.min.js", - "/highlightjs-launch.js" + "/assets/highlightjs-launch.js" ], - "css": "/highlightjs-tabler-theme.css", - "footer": "Official [SQLPage](https://sql.ophir.dev) documentation" + "css": "/assets/highlightjs-and-tabler-theme.css", + "footer": "[Built with SQLPage](https://github.com/sqlpage/SQLPage/tree/main/examples/official-site)" }]')), ('shell', ' +This example shows how to set menu items as active in the navigation, so that they are highlighted in the nav bar. + +In this example you can see that two menu items are created, "Home" and "About" and the "Home" tab is marked as active. +', + json('[{ + "component": "shell", + "title": "SQLPage: SQL websites", + "icon": "database", + "link": "/", + "menu_item": [ + {"title": "Home", "active": true}, + {"title": "About"} + ] + }]')), + + ('shell', ' ### Sharing the shell between multiple pages It is common to want to share the same shell between multiple pages. @@ -948,8 +1450,8 @@ and in `shell.json`: "menu_item": [ {"link": "index.sql", "title": "Home"}, {"title": "Community", "submenu": [ - {"link": "blog.sql", "title": "Blog"}, - {"link": "//github.com/lovasoa/sqlpage", "title": "Github"} + {"link": "/blog.sql", "title": "Blog"}, + {"link": "//github.com/sqlpage/SQLPage", "title": "Github"} ]} ] } @@ -987,7 +1489,7 @@ a "Profile" menu item only to authenticated users, and a "Login" menu item only to unauthenticated users: ```sql -SET $role = ( +set role = ( SELECT role FROM users INNER JOIN sessions ON users.id = sessions.user_id WHERE sessions.session_id = sqlpage.cookie(''session_id'') @@ -1026,22 +1528,47 @@ SELECT ''/'' AS link, TRUE AS fixed_top_menu, ''{"title":"About","icon": "settings","submenu":[{"link":"/safety.sql","title":"Security","icon": "logout"},{"link":"/performance.sql","title":"Performance"}]}'' AS menu_item, - ''{"title":"Examples","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs.sql","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}'' AS menu_item, - ''{"title":"Examples","size":"sm","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs.sql","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}'' AS menu_item, - ''Official [SQLPage](https://sql.ophir.dev) documentation'' as footer; + ''{"title":"Examples","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs/","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}'' AS menu_item, + ''{"title":"Examples","size":"sm","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg","submenu":[{"link":"/examples/tabs/","title":"Tabs","image": "https://upload.wikimedia.org/wikipedia/en/6/6b/Terrestrial_globe.svg"},{"link":"/examples/layouts.sql","title":"Layouts"}]}'' AS menu_item, + ''Official [SQLPage](https://sql-page.com) documentation'' as footer; ``` ', NULL), ('shell', ' -### A page without a shell -SQLPage provides the `shell-empty` component to create a page without a shell. -In this case, the `html` and `body` tags are not generated, and the components are rendered directly in the page -without any styling, navigation bar, footer, or dynamic content. -This is useful when you want to generate a snippet of HTML that can be dynamically included in a larger page. +### Returning custom HTML, XML, plain text, or other formats + +Use `shell-empty` to opt out of SQLPage''s component system and return raw data directly. -Any component whose name starts with `shell` will be considered as a shell component, -so you can also [create your own shell component](custom_components.sql#custom-shell). +By default, SQLPage wraps all your content in a complete HTML page with navigation and styling. +The `shell-empty` component tells SQLPage to skip this HTML wrapper and return only the raw content you specify. +Use it to create endpoints that return things like + - XML (for JSON, use the [json](?component=json) component) + - plain text or markdown content (for instance for consumption by LLMs) + - a custom data format you need + +When using `shell-empty`, you should use the [http_header](component.sql?component=http%5Fheader) component first +to set the correct [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) (like `application/json` or `application/xml`). +', + json('[ + { + "component":"http_header", + "Content-Type":"application/xml" + }, + { + "component":"shell-empty", + "contents": "\n \n 42\n john.doe\n " + } + ]') + ), + ('shell',' +### Generate your own HTML If you generate your own HTML from a SQL query, you can also use the `shell-empty` component to include it in a page. +This is useful when you want to generate a snippet of HTML that can be dynamically included in a larger page. Make sure you know what you are doing, and be careful to escape the HTML properly, -as you are stepping out of the safe SQLPage framework and into the wild world of HTML.', +as you are stepping out of the safe SQLPage framework and into the wild world of HTML. + +In this scenario, you can use the `html` property, which serves as an alias for the `contents` property. +This property improves code readability by clearly indicating that you are generating HTML. +Since SQLPage returns HTML by default, there is no need to specify the content type in the HTTP header. +', json('[{"component":"shell-empty", "html": "\n\n\n My page\n\n\n

My page

\n\n"}]')); diff --git a/examples/official-site/sqlpage/migrations/02_hero_component.sql b/examples/official-site/sqlpage/migrations/02_hero_component.sql index 47dbcd42..d528453b 100644 --- a/examples/official-site/sqlpage/migrations/02_hero_component.sql +++ b/examples/official-site/sqlpage/migrations/02_hero_component.sql @@ -159,7 +159,7 @@ VALUES "description_md": "Documentation for the *SQLPage* low-code web application framework.", "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Lac_de_Zoug.jpg/640px-Lac_de_Zoug.jpg", "link": "/documentation.sql", - "link_text": "Read Documentation !"},' || '{"title": "Fast", "description": "Pages load instantly, even on slow mobile networks.", "icon": "car", "color": "red", "link": "/"},' || '{"title": "Beautiful", "description": "Uses pre-defined components that look professional.", "icon": "eye", "color": "green", "link": "/"},' || '{"title": "Easy", "description_md": "You can teach yourself enough SQL to use [**SQLPage**](https://sql.ophir.dev) in a weekend.", "icon": "sofa", "color": "blue", "link": "/"}' || ']' + "link_text": "Read Documentation !"},' || '{"title": "Fast", "description": "Pages load instantly, even on slow mobile networks.", "icon": "car", "color": "red", "link": "/"},' || '{"title": "Beautiful", "description": "Uses pre-defined components that look professional.", "icon": "eye", "color": "green", "link": "/"},' || '{"title": "Easy", "description_md": "You can teach yourself enough SQL to use [**SQLPage**](https://sql-page.com) in a weekend.", "icon": "sofa", "color": "blue", "link": "/"}' || ']' ) ), ( diff --git a/examples/official-site/sqlpage/migrations/03_alert_component.sql b/examples/official-site/sqlpage/migrations/03_alert_component.sql index 51c59964..0313bcda 100644 --- a/examples/official-site/sqlpage/migrations/03_alert_component.sql +++ b/examples/official-site/sqlpage/migrations/03_alert_component.sql @@ -167,7 +167,7 @@ VALUES ( "important": true, "dismissible": true, "description":"SQLPage is entirely free and open source.", - "link":"https://github.com/lovasoa/SQLPage", + "link":"https://github.com/sqlpage/SQLPage", "link_text":"See source code" }]' ) @@ -182,7 +182,7 @@ VALUES ( "title":"Free and open source", "icon": "free-rights", "color": "info", - "description_md":"*SQLPage* is entirely free and open source. You can **contribute** to it on [GitHub](https://github.com/lovasoa/SQLPage)." + "description_md":"*SQLPage* is entirely free and open source. You can **contribute** to it on [GitHub](https://github.com/sqlpage/SQLPage)." }]' ) ); diff --git a/examples/official-site/sqlpage/migrations/04_http_header.sql b/examples/official-site/sqlpage/migrations/04_http_header.sql index c7f39bfa..1667e426 100644 --- a/examples/official-site/sqlpage/migrations/04_http_header.sql +++ b/examples/official-site/sqlpage/migrations/04_http_header.sql @@ -2,24 +2,25 @@ INSERT INTO component (name, description, icon) VALUES ( 'http_header', - 'An advanced component to set arbitrary HTTP headers: can be used to set a custom caching policy to your pages, or implement custom redirections, for example. - If you are a beginner, you probably don''t need this component. + ' +An advanced component to set arbitrary HTTP headers: can be used to set a custom caching policy to your pages, or implement custom redirections, for example. +If you are a beginner, you probably don''t need this component. - When used, this component has to be the first component in the page, because once the page is sent to the browser, it is too late to change the headers. +When used, this component has to be the first component in the page, because once the page is sent to the browser, it is too late to change the headers. - HTTP headers are additional pieces of information sent with responses to web requests that provide instructions - or metadata about the data being sent — for example, - setting cache control directives to control caching behavior - or specifying the content type of a response. - - Any valid HTTP header name can be used as a top-level parameter for this component. - The examples shown here are just that, examples; and you can create any custom header - if needed simply by declaring it. - - If your header''s name contains a dash or any other special character, - you will have to use your database''s quoting mechanism to declare it. - In standard SQL, you can use double quotes to quote identifiers (like "X-My-Header"), - in Microsoft SQL Server, you can use square brackets (like [X-My-Header]). +HTTP headers are additional pieces of information sent with responses to web requests that provide instructions +or metadata about the data being sent — for example, +setting cache control directives to control caching behavior +or specifying the content type of a response. + +Any valid HTTP header name can be used as a top-level parameter for this component. +The examples shown here are just that, examples; and you can create any custom header +if needed simply by declaring it. + +If your header''s name contains a dash or any other special character, +you will have to use your database''s quoting mechanism to declare it. +In standard SQL, you can use double quotes to quote identifiers (like "X-My-Header"), +in Microsoft SQL Server, you can use square brackets (like [X-My-Header]). ', 'world-www' ); diff --git a/examples/official-site/sqlpage/migrations/05_cookie.sql b/examples/official-site/sqlpage/migrations/05_cookie.sql index 0b7f2998..efd197a3 100644 --- a/examples/official-site/sqlpage/migrations/05_cookie.sql +++ b/examples/official-site/sqlpage/migrations/05_cookie.sql @@ -2,12 +2,15 @@ INSERT INTO component (name, description, icon) VALUES ( 'cookie', - 'Sets a cookie in the client browser, used for session management and storing user-related information. - - This component creates a single cookie. Since cookies need to be set before the response body is sent to the client, - this component should be placed at the top of the page, before any other components that generate output. + ' +Sets a cookie in the client browser, used for session management and storing user-related information. - After being set, a cookie can be accessed anywhere in your SQL code using the `sqlpage.cookie(''cookie_name'')` pseudo-function.', +This component creates a single cookie. Since cookies need to be set before the response body is sent to the client, +this component should be **placed at the top of the page**, before any other components that generate output. + +After being set, a cookie can be accessed anywhere in your SQL code using the `sqlpage.cookie(''cookie_name'')` pseudo-function. + +Note that if your site is accessed over HTTP (and not HTTPS), you have to set `false as secure` to force browsers to accept your cookies.', 'cookie' ); -- Insert the parameters for the http_header component into the parameter table @@ -119,4 +122,4 @@ SELECT ''text'' as component, ''Your name is '' || COALESCE(sqlpage.cookie(''username''), ''not known to us''); ``` ' - ); \ No newline at end of file + ); diff --git a/examples/official-site/sqlpage/migrations/07_authentication.sql b/examples/official-site/sqlpage/migrations/07_authentication.sql index 0af5b327..c759dee8 100644 --- a/examples/official-site/sqlpage/migrations/07_authentication.sql +++ b/examples/official-site/sqlpage/migrations/07_authentication.sql @@ -2,13 +2,41 @@ INSERT INTO component (name, description, icon, introduced_in_version) VALUES ( 'authentication', - 'An advanced component that can be used to create pages with password-restricted access. - When used, this component has to be at the top of your page, because once the page has begun being sent to the browser, it is too late to restrict access to it. - The authentication component checks if the user has sent the correct password, and if not, redirects them to the URL specified in the link parameter. - If you don''t want to re-check the password on every page (which is an expensive operation), - you can check the password only once and store a session token in your database. - You can use the cookie component to set the session token cookie in the client browser, - and then check whether the token matches what you stored in subsequent pages.', + ' +Create pages with password-restricted access. + + +When you want to add user authentication to your SQLPage application, +you have two main options: + +1. The `authentication` component: + - lets you manage usernames and passwords yourself + - does not require any external service + - gives you fine-grained control over + - which pages and actions are protected + - the look of the [login form](?component=login) + - the duration of the session + - the permissions of each user +2. [**Single sign-on**](/sso) + - lets users log in with their existing accounts (like Google, Microsoft, or your organization''s own identity provider) + - requires setting up an external service (Google, Microsoft, etc.) + - frees you from implementing a lot of features like password reset, account creation, user management, etc. + +This page describes the first option. + +When used, this component has to be at the top of your page, +because once the page has begun being sent to the browser, +it is too late to restrict access to it. + +The authentication component checks if the user has sent the correct password, +and if not, redirects them to the URL specified in the link parameter. + +If you don''t want to re-check the password on every page (which is an expensive operation), +you can check the password only once and store a session token in your database +(see the session example below). + +You can use the [cookie component](?component=cookie) to set the session token cookie in the client browser, +and then check whether the token matches what you stored in subsequent pages.', 'lock', '0.7.2' ); @@ -100,12 +128,10 @@ Then, in all the pages that require authentication, you check if the cookie is p You can check if the user has sent the correct password in a form, and if not, redirect them to a login page. -Create a login form in a file called `login.sql`: +Create a login form in a file called `login.sql` that uses the [login component](?component=login): ```sql -select ''form'' as component, ''Authentication'' as title, ''Log in'' as validate, ''create_session_token.sql'' as action; -select ''Username'' as name, ''admin'' as placeholder; -select ''Password'' as name, ''admin'' as placeholder, ''password'' as type; +select ''login'' as component; ``` And then, in `create_session_token.sql` : @@ -158,9 +184,6 @@ RETURNING ### Single sign-on with OIDC (OpenID Connect) If you don''t want to manage your own user database, -you can use OpenID Connect and OAuth2 to authenticate users. -This allows users to log in with their Google, Facebook, or internal company account. - -You will find an example of how to do this in the -[Single sign-on with OIDC](https://github.com/lovasoa/SQLpage/tree/main/examples/single%20sign%20on). +you can [use OpenID Connect and OAuth2](/sso) to authenticate users. +This allows users to log in with their Google, Microsoft, or internal company account. '); diff --git a/examples/official-site/sqlpage/migrations/08_functions.sql b/examples/official-site/sqlpage/migrations/08_functions.sql index cb5d7209..2f5ca8a7 100644 --- a/examples/official-site/sqlpage/migrations/08_functions.sql +++ b/examples/official-site/sqlpage/migrations/08_functions.sql @@ -86,6 +86,8 @@ Log the [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers ```sql INSERT INTO user_agent_log (user_agent) VALUES (sqlpage.header(''user-agent'')); ``` + +If you need access to all headers at once, use [`sqlpage.headers()`](?function=headers) instead. ' ); INSERT INTO sqlpage_function_parameters ( @@ -155,8 +157,11 @@ VALUES ( '0.7.2', 'spy', ' -Hashes a password using the [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. -The resulting hash can be stored in the database and then used with the [authentication component](documentation.sql?component=authentication#component). +Hashes a password with the Argon2id variant and outputs it in the [PHC string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md), ready to store in your users table. + +Every call generates a brand new cryptographic salt so that two people choosing the same password still end up with different hashes, which defeats rainbow-table attacks and lets you safely reveal only the hash. + +Use this function only when creating or resetting a password (for example while inserting a brand new user): it writes the stored value. Later, at login time, the [authentication component](documentation.sql?component=authentication#component) reads the stored hash, hashes the visitor''s password with the embedded salt and parameters, and grants access only if they match. ### Example @@ -254,7 +259,7 @@ Currently running from `/home/user/my_sqlpage_website` The current working directory is the directory from which the SQLPage server process was started. By default, this is also the directory from which `.sql` files are loaded and served. -However, this can be changed by setting the `web_root` [configuration option](https://github.com/lovasoa/SQLpage/blob/main/configuration.md). +However, this can be changed by setting the `web_root` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md). ' ); INSERT INTO sqlpage_functions ( diff --git a/examples/official-site/sqlpage/migrations/09_redirect.sql b/examples/official-site/sqlpage/migrations/09_redirect.sql index 30438de5..eb694212 100644 --- a/examples/official-site/sqlpage/migrations/09_redirect.sql +++ b/examples/official-site/sqlpage/migrations/09_redirect.sql @@ -2,13 +2,38 @@ INSERT INTO component (name, description, icon, introduced_in_version) VALUES ( 'redirect', 'Redirects the user to another page. - This component is useful for implementing redirects after a form submission, - or to redirect users to a login page if they are not logged in. - - Contrary to the http_header component, this component completely stops the execution of the page after it is called, - so it is suitable to use to hide sensitive information from users that are not logged in, for example. - Since it uses an HTTP header to redirect the user, it is not possible to use this component after the page has started being sent to the browser.', +This component helps you: +1. Send users to a different page +1. Stop execution of the current page + +### Conditional logic + +There is no `IF` statement in SQL. Even when you use a [`CASE` expression](https://modern-sql.com/caniuse/case_(simple)), all branches are always evaluated (and only one is returned). + +To conditionally execute a component or a [SQLPage function](/functions.sql), you can use the `redirect` component. +A common use case is error handling. You may want to proceed with the rest of a page only when certain pre-conditions are met. + +```sql +SELECT + ''redirect'' AS component, + ''error_page.sql'' AS link +WHERE NOT your_condition; + +-- The rest of the page is only executed if the condition is true +``` +### Technical limitation + +You must use this component **at the beginning of your SQL file**, before any other components that might send content to the browser. +Since the component needs to tell the browser to go to a different page by sending an *HTTP header*, +it will fail if the HTTP headers have already been sent by the time it is executed. + +> **Important difference from [http_header](?component=http_header)** +> +> This component completely stops the page from running after it''s called. +> This makes it a good choice for protecting sensitive information from unauthorized users. + +', 'arrow-right', '0.7.2' ); diff --git a/examples/official-site/sqlpage/migrations/10_map.sql b/examples/official-site/sqlpage/migrations/10_map.sql index 7053cf01..040614c6 100644 --- a/examples/official-site/sqlpage/migrations/10_map.sql +++ b/examples/official-site/sqlpage/migrations/10_map.sql @@ -1,12 +1,34 @@ -INSERT INTO component (name, description, icon, introduced_in_version) -VALUES ( +INSERT INTO + component (name, description, icon, introduced_in_version) +VALUES + ( 'map', - 'Displays a map with markers on it. Useful in combination with PostgreSQL''s PostGIS or SQLite''s spatialite.', + ' + +## Visualize SQL data on a map. + +The map component displays a custom interactive map with markers on it. + +In its simplest form, the component displays points on a map from a table of latitudes and longitudes. +But it can also be used by cartographers in combination with PostgreSQL''s PostGIS or SQLite''s spatialite, +to create custom visualizations of geospatial data. +Use the `geojson` property to generate rich maps from a GIS database. + +### Example Use Cases + +1. **Store Locator**: Build an interactive map to find the nearest store information using SQL-stored geospatial data. +2. **Delivery Route Optimization**: Visualize the results of delivery route optimization algorithms. +3. **Sales Heatmap**: Identify high-performing regions by mapping sales data stored in SQL. +4. **Real-Time Tracking**: Create dynamic dashboards that track vehicles, assets, or users live using PostGIS or MS SQL Server geospatial time series data. Use the [shell](?component=shell) component to auto-refresh the map. +5. **Demographic Insights**: Map customer demographics or trends geographically to uncover opportunities for growth or better decision-making. +', 'map', '0.8.0' ); + -- Insert the parameters for the http_header component into the parameter table -INSERT INTO parameter ( +INSERT INTO + parameter ( component, name, description, @@ -14,7 +36,8 @@ INSERT INTO parameter ( top_level, optional ) -VALUES ( +VALUES + ( 'map', 'latitude', 'Latitude of the center of the map. If omitted, the map will be centered on its markers.', @@ -38,7 +61,7 @@ VALUES ( TRUE, TRUE ), - ( + ( 'map', 'max_zoom', 'How far the map can be zoomed in. Defaults to 18. Added in v0.15.2.', @@ -46,7 +69,7 @@ VALUES ( TRUE, TRUE ), - ( + ( 'map', 'tile_source', 'Custom map tile images to use, as a URL. Defaults to "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png". Added in v0.15.2.', @@ -54,7 +77,7 @@ VALUES ( TRUE, TRUE ), - ( + ( 'map', 'attribution', 'Text to display at the bottom right of the map. Defaults to "© OpenStreetMap".', @@ -62,6 +85,14 @@ VALUES ( TRUE, TRUE ), + ( + 'map', + 'height', + 'Height of the map, in pixels. Default to 350px', + 'INTEGER', + TRUE, + TRUE + ), ( 'map', 'latitude', @@ -141,21 +172,38 @@ VALUES ( 'INTEGER', FALSE, TRUE - ) - ; + ); + -- Insert an example usage of the map component into the example table -INSERT INTO example (component, description, properties) -VALUES ( +INSERT INTO + example (component, description, properties) +VALUES + ( 'map', - 'Basic example of a map with a marker', - JSON( + ' +### Adding a marker to a map + +Showing how to place a marker on a map. Useful for basic location displays like showing a single office location, event venue, or point of interest. The marker shows basic hover and click interactions. +', + JSON ( '[{ "component": "map" }, { "title": "New Delhi", "latitude": 28.6139, "longitude": 77.2090 }]' ) ), ( 'map', - 'Basic marker defined in GeoJSON. Using [leaflet marker options](https://leafletjs.com/reference.html#marker-option) as GeoJSON properties.', - JSON( + ' +### Advanced map customization using GeoJSON and custom map tiles + +This example demonstrates using topographic map tiles, custom marker styling, +and clickable markers that link to external content - perfect for educational or tourism applications. + +It uses [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) to display polygons and lines. + + - You can generate GeoJSON data from PostGIS geometries using the [`ST_AsGeoJSON`](https://postgis.net/docs/ST_AsGeoJSON.html) function. + - In spatialite, you can use the [`AsGeoJSON`](https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.1.0.html#p3misc) function. + - In MySQL, you can use the [`ST_AsGeoJSON()`](https://dev.mysql.com/doc/refman/8.0/en/spatial-geojson-functions.html#function_st-asgeojson) function. +', + JSON ( '[{ "component": "map", "zoom": 5, "max_zoom": 8, "height": 600, "latitude": -25, "longitude": 28, "tile_source": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", "attribution": "" }, { "icon": "peace", "size": 20, @@ -165,33 +213,45 @@ VALUES ( ), ( 'map', - 'Map of Paris. -Illustrates the use custom styling, and [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) to display a line between two points. -In a real-world scenario, the GeoJSON could be generated by calling PostGIS''s -[`ST_AsGeoJSON`](https://postgis.net/docs/ST_AsGeoJSON.html) or -Spatialite''s [`AsGeoJSON`](https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.1.0.html#p3misc) functions on a geometry column.', - JSON( + ' +### Maps with links and rich descriptions + +Demonstrates how to create an engaging map with custom icons, colors, rich descriptions with markdown support, and connecting points with lines. +Perfect for visualizing multi-dimensional relationships between points on a map, like routes between locations. + +Note that the map tile source is set to a MapTiler map. The API key included in the URL in this demo will not work on your own website. +You should get your own API key at [MapTiler](https://www.maptiler.com/cloud/). +', + JSON ( '[ - { "component": "map", "title": "Paris", "zoom": 11, "latitude": 48.85, "longitude": 2.34 }, + { "component": "map", "title": "Paris", "zoom": 13, "latitude": 48.85, "longitude": 2.34, "tile_source": "https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}.png?key=RwoF6Y3gcKx4OBMbvqOY" }, { "title": "Notre Dame", "icon": "building-castle", "color": "indigo", "latitude": 48.8530, "longitude": 2.3498, "description_md": "A beautiful cathedral.", "link": "https://en.wikipedia.org/wiki/Notre-Dame_de_Paris" }, - { "title": "Eiffel Tower", "icon": "tower", "color": "yellow", "latitude": 48.8584, "longitude": 2.2945, "description_md": "A tall tower. [Wikipedia](https://en.wikipedia.org/wiki/Eiffel_Tower)" }, + { "title": "Eiffel Tower", "icon": "tower", "color": "red", "latitude": 48.8584, "longitude": 2.2945, "description_md": "A tall tower. [Wikipedia](https://en.wikipedia.org/wiki/Eiffel_Tower)" }, { "title": "Tower to Cathedral", "geojson": {"type": "LineString", "coordinates": [[2.2945, 48.8584], [2.3498, 48.8530]]}, "color": "teal", "description": "A nice 45 minutes walk." } ]' ) ), ( 'map', - 'Geometric shapes + ' +### Abstract geometric visualizations -Illustrates the use of GeoJSON to display a square and a circle, without an actual geographical base map, -by setting the `tile_source` parameter to `false`.', - JSON( +Example showing how to create abstract geometric visualizations without a base map. +Useful for displaying spatial data that doesn''t need geographic context, like floor plans, seating charts, +or abstract 2D data visualizations. +', + JSON ( '[ { "component": "map", "tile_source": false }, - { "title": "Square", - "color": "red", "description": "The litteral red square", - "geojson": {"type": "Polygon", "coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]} + { "title": "MySQL", + "color": "red", "description": "This literal red square is defined as a GeoJSON polygon. Each (x,y) coordinate is a JSON array.", + "geojson": {"type": "Polygon", "coordinates": [[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]]]} + }, + { + "title": "SQLite", + "color": "blue", "description": "This 2D shape was generated by a SQL query.", + "geojson": {"type": "Polygon", "coordinates": [[[5, 0], [9, 0], [7, 4], [5, 0]]]} } ]' ) - ); \ No newline at end of file + ); diff --git a/examples/official-site/sqlpage/migrations/11_json.sql b/examples/official-site/sqlpage/migrations/11_json.sql index 8274ec29..8384ab58 100644 --- a/examples/official-site/sqlpage/migrations/11_json.sql +++ b/examples/official-site/sqlpage/migrations/11_json.sql @@ -1,14 +1,35 @@ -INSERT INTO component (name, description, icon, introduced_in_version) -VALUES ( +INSERT INTO + component (name, description, icon, introduced_in_version) +VALUES + ( 'json', - 'For advanced users, allows you to easily build an API over your database. - The json component responds to the current HTTP request with a JSON object. - This component must appear at the top of your SQL file, before any other data has been sent to the browser.', + 'Converts SQL query results into the JSON machine-readable data format. Ideal to quickly build APIs for interfacing with external systems. + +**JSON** is a widely used data format for programmatic data exchange. +For example, you can use it to integrate with web services written in different languages, +with mobile or desktop apps, or with [custom client-side components](/custom_components.sql) inside your SQLPage app. + +Use it when your application needs to expose data to external systems. +If you only need to render standard web pages, +and do not need other software to access your data, +you can ignore this component. + +This component **must appear at the top of your SQL file**, before any other data has been sent to the browser. +An HTTP response can have only a single datatype, and it must be declared in the headers. +So if you have already called the `shell` component, or another traditional HTML component, +you cannot use this component in the same file. + +SQLPage can also return JSON or JSON Lines when the incoming request says it prefers them with an HTTP `Accept` header, so the same `/users.sql` page can show a table in a browser but return raw data to `curl -H "Accept: application/json" http://localhost:8080/users.sql`. + +Use this component when you want to control the payload or force JSON output even for requests that would normally get HTML. +', 'code', '0.9.0' ); + -- Insert the parameters for the http_header component into the parameter table -INSERT INTO parameter ( +INSERT INTO + parameter ( component, name, description, @@ -16,22 +37,110 @@ INSERT INTO parameter ( top_level, optional ) -VALUES ( +VALUES + ( 'json', 'contents', - 'The JSON payload to send. You should use your database''s built-in json functions to build the value to enter here.', + 'A single JSON payload to send. You can use your database''s built-in json functions to build the value to enter here. If not provided, the contents will be taken from the next SQL statements and rendered as a JSON array.', + 'TEXT', + TRUE, + TRUE + ), + ( + 'json', + 'type', + 'The type of the JSON payload to send: "array", "jsonlines", or "sse". +In "array" mode, each query result is rendered as a JSON object in a single top-level array. +In "jsonlines" mode, results are rendered as JSON objects in separate lines, without a top-level array. +In "sse" mode, results are rendered as JSON objects in separate lines, prefixed by "data: ", which allows you to read the results as server-sent events in real-time from javascript.', 'TEXT', TRUE, - FALSE + TRUE ); + -- Insert an example usage of the http_header component into the example table -INSERT INTO example (component, description) -VALUES ( +INSERT INTO + example (component, description) +VALUES + ( 'json', ' -Creates an API endpoint that will allow developers to easily query a list of users stored in your database. +## Send query results as a single JSON array: `''array'' as type` + +The default `array` mode sends the query results as a single JSON array. + +If a query returns an error, the array will contain an object with an `error` property. + +If multiple queries are executed, all query results will be concatenated into a single array +of heterogeneous objects. + +### SQL + +```sql +select ''json'' AS component; +select * from users; +``` + +### Result + +```json +[ + {"username":"James","userid":1}, + {"username":"John","userid":2} +] +``` + +Clients can also receive JSON or JSON Lines automatically by requesting the same SQL file with an HTTP `Accept` header such as `application/json` or `application/x-ndjson` when the component is omitted, for example: + +``` +curl -H "Accept: application/json" http://localhost:8080/users.sql +``` + ' + ), + ( + 'json', + ' +## Send a single JSON object: `''jsonlines'' as type` + +In `jsonlines` mode, each query result is rendered as a JSON object in a separate line, +without a top-level array. + +If there is a single query result, the response will be a valid JSON object. +If there are multiple query results, you will need to parse each line of the response as a separate JSON object. + +If a query returns an error, the response will be a JSON object with an `error` property. -You should use the json functions provided by your database to form the value you pass to the `contents` property. +### SQL + +The following SQL creates an API endpoint that takes a `user_id` URL parameter +and returns a single JSON object containing the user''s details, with one json object key per column in the `users` table. + +```sql +select ''json'' AS component, ''jsonlines'' AS type; +select * from users where id = $user_id LIMIT 1; +``` + +> Note the `LIMIT 1` clause. The `jsonlines` type will send one JSON object per result row, +> separated only by a single newline character (\n). +> So if your query returns multiple rows, the result will not be a single valid JSON object, +> like most JSON parsers expect. + +### Result + +```json +{ "username":"James", "userid":1 } +``` +' + ), + ( + 'json', + ' +## Create a complex API endpoint: the `''contents''` property + +You can create an API endpoint that will return a JSON value in any format you want, +to implement a complex API. + +You should use [the json functions provided by your database](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) to form the value you pass to the `contents` property. To build a json array out of rows from the database, you can use: - `json_group_array()` in SQLite, - `json_agg()` in Postgres, or @@ -63,5 +172,40 @@ This will return a JSON response that looks like this: } ``` +If you want to handle custom API routes, like `POST /api/users/:id`, +you can use + - the [`404.sql` file](/your-first-sql-website/custom_urls.sql) to handle the request despite the URL not matching any file, + - the [`request_method` function](/functions.sql?function=request_method#function) to differentiate between GET and POST requests, + - and the [`path` function](/functions.sql?function=path#function) to extract the `:id` parameter from the URL. ' - ); \ No newline at end of file + ), + ( + 'json', + ' +## Access query results in real-time with server-sent events: `''sse'' as type` + +Using server-sent events, you can stream large query results to the client in real-time, +row by row. + +This allows building sophisticated dynamic web applications that will start processing and displaying +the first rows of data in the browser while the database server is still processing the end of the query. + +### SQL + +```sql +select ''json'' AS component, ''sse'' AS type; +select * from users; +``` + +### JavaScript + +```javascript +const eventSource = new EventSource("users.sql"); +eventSource.onmessage = function (event) { + const user = JSON.parse(event.data); + console.log(user.username); +} +eventSource.onerror = () => eventSource.close(); // do not reconnect after reading all the data +``` +' + ); diff --git a/examples/official-site/sqlpage/migrations/12_blog.sql b/examples/official-site/sqlpage/migrations/12_blog.sql index f40cc43d..773e29ec 100644 --- a/examples/official-site/sqlpage/migrations/12_blog.sql +++ b/examples/official-site/sqlpage/migrations/12_blog.sql @@ -15,37 +15,80 @@ VALUES 'code-minus', '2023-08-03', ' -**No-Code vs Low-Code: Why Writing an App in SQL Makes Sense** 🚀 -================================================================= +# Choosing Your Path: No-Code, Low-Code, or SQL-Based Development -So, you''ve got this brilliant app idea that''s been keeping you up at night. You want it to shine, sparkle, and dazzle users. But here''s the catch: you''re not exactly a coding wizard. No worries, the tech world has got you covered with two charming suitors – No-Code and Low-Code platforms. 🎩💻 +The platform you select shapes the entire trajectory of your application. +Each approach offers distinct advantages, yet demands different compromises - a choice that warrants careful consideration. -The Tempting Allure of No-Code ------------------------------- +## No-Code Platforms: Speed with Limitations -**No-Code tools**, oh sweet simplicity! They sweep you off your feet, promising a land of no syntax-induced headaches. You don''t need to be on first-name terms with SQL or worry about the semi-colon''s mood swings. Plus, you get to play the grand designer, arranging elements like a digital Picasso. +No-Code platforms present a visual canvas for building applications without traditional programming. Whilst brilliant for rapid prototypes and straightforward departmental tools, they falter when confronted with complexity and scale. -But, hold up, there''s a twist in this love story. As the relationship deepens, you discover the truth – No-Code isn''t that great at handling complex data manipulations. Your app''s smooth moves suddenly stumble, and you realize the sleek exterior is covering up some cracks. When the app grows, maintenance turns into a melodrama, and waving goodbye to version control feels like a heartbreak. 💔 +**Best suited to**: Quick internal tools and simple workflows -The Charming Proposal of Low-Code ---------------------------------- +### **Notable examples** -Now enters the **Low-Code** hero, complete with a dapper suit and a trunk full of powerful tools. With Low-Code, you''re in the driver''s seat, crafting every detail of your app with elegance and precision. You''re not just the designer; you''re the maestro orchestrating a symphony of functionality. + - [NocoBase](https://www.nocobase.com/) + - [NocoDB](https://www.nocodb.com/) + - [Saltcorn](https://github.com/saltcorn/saltcorn) -But don''t be fooled by the fairy-tale facade – some Low-Code sweethearts have a hidden agenda. They entice you with their ease and beauty, but as your app grows, you discover they''re trying to lock you in. A switch to something more substantial means starting from scratch, leaving you with a déjà vu of rebuilding your app''s entire world. -The SQLPage Love Story 💘 -------------------------- +## Low-Code Platforms: The Flexible Middle Ground -And then, there''s **SQLPage** – the dashing knight that marries the best of both worlds. Lightweight, easy to self-host, and oh-so-elegant, SQLPage dances with your PostgreSQL database, effortlessly creating captivating web apps. It''s like a matchmaking genius, uniting your SQL skills with stunning visual displays. 🕺💃 +These platforms artfully combine visual development with traditional coding. They maintain the power of custom code whilst accelerating development through carefully designed components. -But here''s the real showstopper – SQLPage doesn''t force you to learn new tricks. It''s all about _standard_ SQL, your old pal from the database kingdom. No code voodoo, no convoluted syntax – just the language you already know and love. And those worries about slow-loading web pages? Say goodbye to buffering frustration; SQLPage websites are sleek, fast, and utterly mesmerizing. +**Best suited to**: Complex applications requiring both speed and customisation -So, next time you''re torn between No-Code''s enchantment and Low-Code''s embrace, remember the charming SQLPage love story. It''s the fairy-tale ending where you''re in control, your data thrives, and your app''s journey grows without painful rewrites. 👑📊 +### **Notable examples** -Give your app the love it deserves – the SQLPage kind of love.💕 - ' - ); + - [Budibase](https://budibase.com/) + - [Directus](https://github.com/directus/directus) + - [Rowy](https://github.com/rowyio/rowy) + +## SQL-Based Development: Elegant Simplicity + +SQLPage offers a refreshingly direct approach: pure SQL-driven web applications. + +For those versed in SQL, it enables sophisticated data-driven applications without the overhead of additional frameworks. + +**Best suited to**: Data-centric applications and dashboards + +**Details**: [SQLPage on GitHub](https://github.com/sqlpage/SQLPage) + +## The AI Revolution in Development + +The emergence of Large Language Models (LLMs) has fundamentally shifted the landscape of application development. Tools that once demanded extensive coding expertise have become remarkably more accessible. AI assistants like ChatGPT excel particularly at generating SQL queries and database operations, making SQL-based platforms surprisingly approachable even for those with limited database experience. These AI companions serve as expert pair programmers, offering suggestions, debugging assistance, and ready-to-use code snippets. + +This transformation especially benefits platforms like SQLPage, where the AI''s prowess in SQL generation can bridge the traditional expertise gap. Even complex queries and database operations can be created through natural language conversations with AI assistants, democratising access to sophisticated data manipulation capabilities. + +## Making an Informed Choice + +Selecting the right development approach requires weighing multiple factors against your project''s specific needs. + +Consider these key decision points to guide your platform selection: + +### **Time Constraints** + - Immediate delivery required → No-Code + - Several days available → SQLPage or Low-Code + +### **Data Complexity** + - Structured data manipulation → SQLPage + - Complex workflows → Low-Code + +### **Team Expertise** + - SQL skills → SQLPage + - Limited technical expertise → No-Code + - Varied technical capabilities → Low-Code + +### **Control Requirements** + - Precise data layer control → SQLPage + - Visual design flexibility → Low-Code + - Speed over customisation → No-Code + +## Further Investigation + +For a thorough demonstration of SQLPage''s capabilities: [Building a Full Web Application with SQLPage](https://www.youtube.com/watch?v=mXdgmSdaXkg) +'); INSERT INTO blog_posts (title, description, icon, created_at, external_url) VALUES ( diff --git a/examples/official-site/sqlpage/migrations/13_tab.sql b/examples/official-site/sqlpage/migrations/13_tab.sql index 3f4c456a..1755404a 100644 --- a/examples/official-site/sqlpage/migrations/13_tab.sql +++ b/examples/official-site/sqlpage/migrations/13_tab.sql @@ -16,7 +16,7 @@ INSERT INTO parameter ( VALUES ( 'tab', 'title', - 'Text to display on the tab.', + 'Text to display on the tab. If link is not set, the link will be the current page with a ''$tab'' parameter set to the tab''s title. If ''id'' is set, the page will be scrolled to the tab.', 'TEXT', FALSE, FALSE @@ -80,17 +80,29 @@ To implement contents that change based on the active tab, use the `tab` paramet For example, if the page is `/my-page.sql`, then the first tab will have a link of `/my-page.sql?tab=My+First+tab`. You could then for instance display contents coming from the database based on the value of the `tab` parameter. -For instance: `SELECT ''text'' AS component, contents_md FROM my_page_contents WHERE tab = $tab` +For instance: `SELECT ''text'' AS component, contents_md FROM my_page_contents WHERE tab = $tab`. +Or you could write different queries for different tabs and use the `$tab` parameter with a static value in a where clause to switch between tabs: + +```sql +select ''tab'' as component; +select ''Projects'' as title, $tab = ''Projects'' as active; +select ''Tasks'' as title, $tab = ''Tasks'' as active; + +select ''table'' as component; + +select * from my_projects where $tab = ''Projects''; +select * from my_tasks where $tab = ''Tasks''; +``` Note that the example below is completely static, and does not use the `tab` parameter to actually switch between tabs. -View the [dynamic tabs example](examples/tabs.sql). +View the [dynamic tabs example](/examples/tabs/). ', JSON( '[ { "component": "tab" }, - { "title": "My First tab", "active": true }, - { "title": "This is tab two" }, - { "title": "Third tab is crazy" } + { "title": "This tab does not exist", "active": true, "link": "?component=tab&tab=tab_1" }, + { "title": "I am not a true tab", "link": "?component=tab&tab=tab_2" }, + { "title": "Do not click here", "link": "?component=tab&tab=tab_3" } ]' ) ), @@ -101,7 +113,7 @@ View the [dynamic tabs example](examples/tabs.sql). '[ { "component": "tab", "center": true }, { "title": "Hero", "link": "?component=hero#component", "icon": "home", "description": "The hero component is a full-width banner with a title and an image." }, - { "title": "Tab", "active": true, "link": "?component=tab#component", "icon": "user", "color": "dark" }, + { "title": "Tab", "link": "?component=tab#component", "icon": "user", "color": "purple" }, { "title": "Card", "link": "?component=card#component", "icon": "credit-card" } ]' ) diff --git a/examples/official-site/sqlpage/migrations/16_timeline.sql b/examples/official-site/sqlpage/migrations/16_timeline.sql index bad83e7d..78f19ba5 100644 --- a/examples/official-site/sqlpage/migrations/16_timeline.sql +++ b/examples/official-site/sqlpage/migrations/16_timeline.sql @@ -95,7 +95,7 @@ VALUES ( JSON( '[ { "component": "timeline" }, - { "title": "v0.13.0 was just released !", "link": "https://github.com/lovasoa/SQLpage/releases/", "date": "2023-10-16", "icon": "brand-github", "color": "green", "description_md": "This version introduces the `timeline` component." }, + { "title": "v0.13.0 was just released !", "link": "https://github.com/sqlpage/SQLPage/releases/", "date": "2023-10-16", "icon": "brand-github", "color": "green", "description_md": "This version introduces the `timeline` component." }, { "title": "They are talking about us...", "description_md": "[This article](https://www.postgresql.org/about/news/announcing-sqlpage-build-dynamic-web-applications-in-sql-2672/) on the official PostgreSQL website mentions SQLPage.", "date": "2023-07-12", "icon": "database", "color": "blue" } ]' ) diff --git a/examples/official-site/sqlpage/migrations/18_button.sql b/examples/official-site/sqlpage/migrations/18_button.sql index 8f354eab..2f6ad4ca 100644 --- a/examples/official-site/sqlpage/migrations/18_button.sql +++ b/examples/official-site/sqlpage/migrations/18_button.sql @@ -12,7 +12,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('shape', 'Shape of the buttons (e.g., pill, square)', 'TEXT', TRUE, TRUE), -- Item-level parameters (for each button) ('link', 'The URL to which the button should navigate when clicked. If the form attribute is specified, then this overrides the page to which the form is submitted.', 'URL', FALSE, TRUE), - ('color', 'The color of the button (e.g., red, green, blue, but also primary, warning, danger, etc.).', 'COLOR', FALSE, TRUE), + ('color', 'The color of the button (e.g., red, green, blue, but also primary, warning, danger, etc.). Only base color names are supported, not variations like "blue-lt" or "gray-300". Use a custom CSS stylesheet to further customize the colors.', 'COLOR', FALSE, TRUE), ('title', 'The text displayed on the button.', 'TEXT', FALSE, TRUE), ('tooltip', 'Text displayed when the user hovers over the button.', 'TEXT', FALSE, TRUE), ('disabled', 'Whether the button is disabled or not.', 'BOOLEAN', FALSE, TRUE), diff --git a/examples/official-site/sqlpage/migrations/20_variables_function.sql b/examples/official-site/sqlpage/migrations/20_variables_function.sql index 69fe1f51..79228569 100644 --- a/examples/official-site/sqlpage/migrations/20_variables_function.sql +++ b/examples/official-site/sqlpage/migrations/20_variables_function.sql @@ -9,9 +9,28 @@ VALUES ( 'variables', '0.15.0', 'variable', - 'Returns a JSON string containing all variables passed as URL parameters or posted through a form. + 'Returns a JSON string containing variables from the HTTP request and user-defined variables. -The database''s json handling functions can then be used to process the data. +The [database''s json handling functions](/blog?post=JSON+in+SQL%3A+A+Comprehensive+Guide) can then be used to process the data. + +## Variable Types + +SQLPage distinguishes between three types of variables: + +- **GET variables**: URL parameters from the [query string](https://en.wikipedia.org/wiki/Query_string) (immutable) +- **POST variables**: Values from form fields [submitted](https://en.wikipedia.org/wiki/POST_(HTTP)#Use_for_submitting_web_forms) by the user (immutable) +- **SET variables**: User-defined variables created with the `SET` command (mutable) + +For more information about SQLPage variables, see the [*SQL in SQLPage* guide](/extensions-to-sql). + +## Usage + +- `sqlpage.variables()` - returns all variables (GET, POST, and SET combined). When multiple variables of the same name are present, the order of precedence is: set > post > get. +- `sqlpage.variables(''get'')` - returns only URL parameters +- `sqlpage.variables(''post'')` - returns only POST form data +- `sqlpage.variables(''set'')` - returns only user-defined variables created with `SET` + +When a SET variable has the same name as a GET or POST variable, the SET variable takes precedence in the combined result. ## Example: a form with a variable number of fields @@ -59,7 +78,7 @@ FROM OPENJSON(sqlpage.variables(''post'')); #### In MySQL -MySQL has [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html), +MySQL has [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html), and [`JSON_KEYS`](https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-keys) which are a little bit less straightforward to use: @@ -95,6 +114,6 @@ VALUES ( 'variables', 1, 'method', - 'Optional. The HTTP request method (GET or POST). Must be a literal string. When not provided, all variables are returned.', + 'Optional. Filter variables by source: ''get'' (URL parameters), ''post'' (form data), or ''set'' (user-defined variables). When not provided, all variables are returned with SET variables taking precedence over request parameters.', 'TEXT' ); diff --git a/examples/official-site/sqlpage/migrations/24_read_file_as_data_url.sql b/examples/official-site/sqlpage/migrations/24_read_file_as_data_url.sql index d9b91078..eba68adc 100644 --- a/examples/official-site/sqlpage/migrations/24_read_file_as_data_url.sql +++ b/examples/official-site/sqlpage/migrations/24_read_file_as_data_url.sql @@ -12,8 +12,9 @@ VALUES ( 'Returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) containing the contents of the given file. -The file path is relative to the `web root` directory, which is the directory from which your website is served -(not necessarily the directory SQLPage is launched from). +The file path is relative to the `web root` directory, which is the directory from which your website is served. +By default, this is the directory SQLPage is launched from, but you can change it +with the `web_root` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md). If the given argument is null, the function will return null. diff --git a/examples/official-site/sqlpage/migrations/26_v0.17_release.sql b/examples/official-site/sqlpage/migrations/26_v0.17_release.sql index 9f9001e6..60bff599 100644 --- a/examples/official-site/sqlpage/migrations/26_v0.17_release.sql +++ b/examples/official-site/sqlpage/migrations/26_v0.17_release.sql @@ -32,7 +32,7 @@ You can access the temporary file path with the new [`sqlpage.uploaded_file_path`](/functions.sql?function=uploaded_file_path#function) function. You can then persist the upload as a permanent file on the server with the -[`sqlpage.exec`](https://sql.ophir.dev/functions.sql?function=exec#function) function: +[`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) function: ```sql set file_path = sqlpage.uploaded_file_path(''profile_picture''); @@ -40,15 +40,15 @@ select sqlpage.exec(''mv'', $file_path, ''/path/to/my/file''); ``` or you can store it directly in a database table with the new -[`sqlpage.read_file_as_data_url`](https://sql.ophir.dev/functions.sql?function=read_file_as_data_url#function) and -[`sqlpage.read_file_as_text`](https://sql.ophir.dev/functions.sql?function=read_file_as_text#function) functions: +[`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file_as_data_url#function) and +[`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file_as_text#function) functions: ```sql insert into files (url) values (sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path(''profile_picture''))) returning ''text'' as component, ''Uploaded new file with id: '' || id as contents; ``` -The maximum size of uploaded files is configurable with the [`max_uploaded_file_size`](https://github.com/lovasoa/SQLpage/blob/main/configuration.md) +The maximum size of uploaded files is configurable with the [`max_uploaded_file_size`](https://github.com/sqlpage/SQLPage/blob/main/configuration.md) configuration parameter. By default, it is set to 5 MiB. ### Parsing CSV files @@ -90,10 +90,10 @@ select upper(name), date_part(''year'', CURRENT_DATE) - cast(age as int) from cs #### Handle uploaded files - - [`sqlpage.uploaded_file_path`](https://sql.ophir.dev/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql.ophir.dev/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location. - - [`sqlpage.uploaded_file_mime_type`](https://sql.ophir.dev/functions.sql?function=uploaded_file_mime_type#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it. + - [`sqlpage.uploaded_file_path`](https://sql-page.com/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location. + - [`sqlpage.uploaded_file_mime_type`](https://sql-page.com/functions.sql?function=uploaded_file_mime_type#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it. - The new [*Image gallery* example](https://github.com/lovasoa/SQLpage/tree/main/examples/image%20gallery%20with%20user%20uploads) + The new [*Image gallery* example](https://github.com/sqlpage/SQLPage/tree/main/examples/image%20gallery%20with%20user%20uploads) in the official repository shows how to use these functions to create a simple image gallery with user uploads. #### Read files @@ -101,8 +101,8 @@ in the official repository shows how to use these functions to create a simple i These new functions are useful to read the contents of a file uploaded by the user, but can also be used to read any file on the computer where SQLPage is running: - - [`sqlpage.read_file_as_text`](https://sql.ophir.dev/functions.sql?function=read_file_as_text#function) reads the contents of a file on the server and returns a text string. - - [`sqlpage.read_file_as_data_url`](https://sql.ophir.dev/functions.sql?function=read_file_as_data_url#function) reads the contents of a file on the server and returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful to embed images directly in web pages, or make link + - [`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file_as_text#function) reads the contents of a file on the server and returns a text string. + - [`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file_as_data_url#function) reads the contents of a file on the server and returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful to embed images directly in web pages, or make link ## HTTPS diff --git a/examples/official-site/sqlpage/migrations/29_divider_component.sql b/examples/official-site/sqlpage/migrations/29_divider_component.sql index 82402e6a..3acaa4e4 100644 --- a/examples/official-site/sqlpage/migrations/29_divider_component.sql +++ b/examples/official-site/sqlpage/migrations/29_divider_component.sql @@ -37,29 +37,70 @@ VALUES ( 'COLOR', TRUE, TRUE + ), + ( + 'divider', + 'size', + 'The size of the divider text, from 1 to 6.', + 'INTEGER', + TRUE, + TRUE + ), + ( + 'divider', + 'bold', + 'Whether the text is bold.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'divider', + 'italics', + 'Whether the text is italicized.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'divider', + 'underline', + 'Whether the text is underlined.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'divider', + 'link', + 'URL of the link for the divider text. Available only when contents is present.', + 'URL', + TRUE, + TRUE ); + -- Insert example(s) for the component INSERT INTO example(component, description, properties) -VALUES +VALUES ( 'divider', - 'A divider with centered text', + 'An empty divider', JSON( '[ { - "component":"divider", - "contents":"Hello" + "component":"divider" } ]' ) ), ( 'divider', - 'An empty divider', + 'A divider with centered text', JSON( '[ { - "component":"divider" + "component":"divider", + "contents":"Hello" } ]' ) @@ -79,15 +120,33 @@ VALUES ), ( 'divider', - 'A divider with blue text at right', + 'A divider with blue text and a link', JSON( '[ { "component":"divider", - "contents":"Hello", - "position":"right", + "contents":"SQLPage components", + "link":"/documentation.sql", "color":"blue" } ]' ) + ), + ( + 'divider', + 'A divider with bold, italic, and underlined text', + JSON( + '[ + { + "component":"divider", + "contents":"Important notice", + "position":"left", + "color":"red", + "size":5, + "bold":true, + "italics":true, + "underline":true + } + ]' + ) ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/30_breadcrumb_component.sql b/examples/official-site/sqlpage/migrations/30_breadcrumb_component.sql index 5897a51c..6ec48379 100644 --- a/examples/official-site/sqlpage/migrations/30_breadcrumb_component.sql +++ b/examples/official-site/sqlpage/migrations/30_breadcrumb_component.sql @@ -57,8 +57,8 @@ VALUES '[ {"component":"breadcrumb"}, {"title":"Home","link":"/"}, - {"title":"Components"}, - {"title":"Breadcrumb"} + {"title":"Components", "link":"/documentation.sql"}, + {"title":"Breadcrumb", "link":"?component=breadcrumb"} ]' ) ), @@ -68,9 +68,9 @@ VALUES JSON( '[ {"component":"breadcrumb"}, - {"title":"Home","link":"/", "active": true}, - {"title":"Articles","link":"blog.sql","description":"Stay informed with the latest news"}, - {"title":"No code vs. Low Code","link":"/blog.sql?post=SQLPage versus No-Code tools"} + {"title":"Home","link":"/","active": true}, + {"title":"Articles","link":"/blog.sql","description":"Stay informed with the latest news"}, + {"title":"JSON in SQL","link":"/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide", "description": "Learn advanced json functions for MySQL, SQLite, PostgreSQL, and SQL Server" } ]' ) ); diff --git a/examples/official-site/sqlpage/migrations/31_card_docs_update.sql b/examples/official-site/sqlpage/migrations/31_card_docs_update.sql index 901f7c76..d3a5f00c 100644 --- a/examples/official-site/sqlpage/migrations/31_card_docs_update.sql +++ b/examples/official-site/sqlpage/migrations/31_card_docs_update.sql @@ -24,7 +24,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('footer_link', 'An URL to which the user should be taken when they click on the footer.', 'URL', FALSE, TRUE), ('style', 'Inline style property to your iframe embed code. For example "background-color: #FFFFFF"', 'TEXT', FALSE, TRUE), ('icon', 'Name of an icon to display on the left side of the card.', 'ICON', FALSE, TRUE), - ('color', 'The name of a color, to be displayed on the left of the card to highlight it.', 'COLOR', FALSE, TRUE), + ('color', 'The name of a color, to be displayed on the left of the card to highlight it. If the embed parameter is enabled and you don''t have a title or description, this parameter won''t apply.', 'COLOR', FALSE, TRUE), ('background_color', 'The background color of the card.', 'COLOR', FALSE, TRUE), ('active', 'Whether this item in the grid is considered "active". Active items are displayed more prominently.', 'BOOLEAN', FALSE, TRUE), ('width', 'The width of the card, between 1 (smallest) and 12 (full-width). The default width is 3, resulting in 4 cards per line.', 'INTEGER', FALSE, TRUE) @@ -45,7 +45,7 @@ INSERT INTO example(component, description, properties) VALUES ('card', 'A beautiful card grid with bells and whistles, showing examples of SQLPage features.', json('[{"component":"card", "title":"Popular SQLPage features", "columns": 2}, {"title": "Download as spreadsheet", "link": "?component=csv#component", "description": "Using the CSV component, you can download your data as a spreadsheet.", "icon":"file-plus", "color": "green", "footer_md": "SQLPage can both [read](?component=form#component) and [write](?component=csv#component) **CSV** files."}, - {"title": "Custom components", "link": "/custom_components.sql", "description": "If you know some HTML, you can create your own components for your application.", "icon":"code", "color": "orange", "footer_md": "You can look at the [source of the official components](https://github.com/lovasoa/SQLpage/tree/main/sqlpage/templates) for inspiration."} + {"title": "Custom components", "link": "/custom_components.sql", "description": "If you know some HTML, you can create your own components for your application.", "icon":"code", "color": "orange", "footer_md": "You can look at the [source of the official components](https://github.com/sqlpage/SQLPage/tree/main/sqlpage/templates) for inspiration."} ]')), ('card', 'You can use cards to display a dashboard with quick access to important information. Use [markdown](https://www.markdownguide.org/basic-syntax) to format the text.', json('[ @@ -72,6 +72,6 @@ INSERT INTO example(component, description, properties) VALUES ('card', 'Cards with remote content', json('[ {"component":"card", "title":"Card with embedded remote content", "columns": 2}, - {"title": "Embedded Chart", "embed": "/examples/chart.sql?_sqlpage_embed", "footer_md": "You can find the sql file that generates the chart [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/chart.sql)" }, + {"title": "Embedded Chart", "embed": "/examples/chart.sql?_sqlpage_embed" }, {"title": "Embedded Video", "embed": "https://www.youtube.com/embed/mXdgmSdaXkg", "allow": "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", "embed_mode": "iframe", "height": "350" } ]')); diff --git a/examples/official-site/sqlpage/migrations/33_blog_v018.sql b/examples/official-site/sqlpage/migrations/33_blog_v018.sql index ff23d1a3..627b6d97 100644 --- a/examples/official-site/sqlpage/migrations/33_blog_v018.sql +++ b/examples/official-site/sqlpage/migrations/33_blog_v018.sql @@ -7,23 +7,23 @@ VALUES 'brand-youtube', '2024-01-29', ' -[SQLPage](https://sql.ophir.dev) is a small web server that renders your SQL queries as beautiful interactive websites. This release has seen significant new features and fixes from new contributors, which is great and show the health of the project ! If you feel something is missing or isn''t working quite right, all your contributions are always welcome. +[SQLPage](https://sql-page.com) is a small web server that renders your SQL queries as beautiful interactive websites. This release has seen significant new features and fixes from new contributors, which is great and show the health of the project ! If you feel something is missing or isn''t working quite right, all your contributions are always welcome. On a side note, I [gave a talk about SQLPage last December at PGConf.eu](https://www.youtube.com/watch?v=mXdgmSdaXkg). It is a great detailed introduction to SQLPage, and I recommend it if you want to learn more about the project. 1. **New `tracking` component for beautiful and compact status reports:** This feature adds a new way to display status reports, making them more visually appealing and concise. - 1. ![screenshot](https://github.com/lovasoa/SQLpage/assets/552629/3e792953-3870-469d-a01d-898316b2ab32) + 1. ![screenshot](https://github.com/sqlpage/SQLPage/assets/552629/3e792953-3870-469d-a01d-898316b2ab32) 3. **New `divider` component to add a horizontal line between other components:** This simple yet useful addition allows for better separation of elements on your pages. - 1. ![image](https://github.com/lovasoa/SQLpage/assets/552629/09a2cc77-3b37-401f-ab3e-441637a2c022) + 1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/09a2cc77-3b37-401f-ab3e-441637a2c022) 5. **New `breadcrumb` component to display a breadcrumb navigation bar:** This component helps users navigate through your website''s hierarchical structure, providing a clear path back to the homepage. - 1. ![image](https://github.com/lovasoa/SQLpage/assets/552629/cbf2174a-1d75-499e-9d6b-e111136dbbbc) + 1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/cbf2174a-1d75-499e-9d6b-e111136dbbbc) 8. **Multi-column layouts with `embed` attribute in `card` component:** This feature enables you to create more complex and dynamic layouts within cards. - 1. ![image](https://github.com/lovasoa/SQLpage/assets/552629/3f4435f0-d89b-424e-8b8a-39385a61d5ad) + 1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/3f4435f0-d89b-424e-8b8a-39385a61d5ad) 6. **Customizable y-axis step size in `chart` component with `ystep` attribute:** This feature gives you more control over the chart''s appearance, especially for situations with multiple series. diff --git a/examples/official-site/sqlpage/migrations/34_carousel.sql b/examples/official-site/sqlpage/migrations/34_carousel.sql index bff821ec..6736ccff 100644 --- a/examples/official-site/sqlpage/migrations/34_carousel.sql +++ b/examples/official-site/sqlpage/migrations/34_carousel.sql @@ -54,7 +54,7 @@ VALUES TRUE, TRUE ), - ( + ( 'carousel', 'auto', 'Whether to automatically cycle through the carousel items. Default is false.', @@ -78,6 +78,14 @@ VALUES TRUE, TRUE ), + ( + 'carousel', + 'delay', + 'Specify the delay, in milliseconds, between two images.', + 'INTEGER', + TRUE, + TRUE + ), ( 'carousel', 'image', @@ -109,6 +117,22 @@ VALUES 'TEXT', FALSE, TRUE + ), + ( + 'carousel', + 'width', + 'The width of the image, in pixels.', + 'INTEGER', + FALSE, + TRUE + ), + ( + 'carousel', + 'height', + 'The height of the image, in pixels.', + 'INTEGER', + FALSE, + TRUE ); -- Insert example(s) for the component INSERT INTO example(component, description, properties) @@ -128,9 +152,9 @@ VALUES ( 'An advanced example of carousel with controls', JSON( '[ - {"component":"carousel","title":"Cats","width":6,"center":true,"controls":true,"auto":true}, - {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/2/29/Cat_Sphynx._Kittens._img_11.jpg/1024px-Cat_Sphynx._Kittens._img_11.jpg","title":"A first cat","description":"The cat (Felis catus), commonly referred to as the domestic cat or house cat, is the only domesticated species in the family Felidae."}, - {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Cat_close-up_2004_b.jpg/1280px-Cat_close-up_2004_b.jpg","title":"Another cat"} + {"component":"carousel","title":"SQL web apps","width":6, "center":true,"controls":true,"auto":true}, + {"image":"/sqlpage_cover_image.webp","title":"SQLPage is modern","description":"Built by engineers who have built so many web applications the old way, they decided they just wouldn''t anymore.", "height": 512}, + {"image":"/sqlpage_illustration_alien.webp","title":"SQLPage is easy", "description":"SQLPage connects to your database, then it turns your SQL queries into nice websites.", "height": 512} ]' ) ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index 0af9fa69..6ff2979d 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -247,18 +247,18 @@ select ''shell-empty'' as component; select ''rss'' as component, ''SQLPage blog'' as title, - ''https://sql.ophir.dev/blog.sql'' as link, + ''https://sql-page.com/blog.sql'' as link, ''latest news about SQLpage'' as description, ''en'' as language, ''Technology'' as category, FALSE as explicit, - ''https://sql.ophir.dev/favicon.ico'' as image_url, + ''https://sql-page.com/favicon.ico'' as image_url, ''Ophir Lojkine'' as author, ''https://github.com/sponsors/lovasoa'' as funding_url, ''episodic'' as type; select ''Hello everyone !'' as title, - ''https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, + ''https://sql-page.com/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description, ''http://127.0.0.1:8080/sqlpage_introduction_video.webm'' as enclosure_url, 123456789 as enclosure_length, diff --git a/examples/official-site/sqlpage/migrations/38_run_sql.sql b/examples/official-site/sqlpage/migrations/38_run_sql.sql index e8464030..4db6a7e8 100644 --- a/examples/official-site/sqlpage/migrations/38_run_sql.sql +++ b/examples/official-site/sqlpage/migrations/38_run_sql.sql @@ -26,11 +26,28 @@ select ''dynamic'' as component, sqlpage.run_sql(''common_header.sql'') as prope #### Notes - - **recursion**: you can use `run_sql` to include a file that itself includes another file, and so on. However, be careful to avoid infinite loops. SQLPage will throw an error if the inclusion depth is superior to 8. - - **security**: be careful when using `run_sql` to include files. Never use `run_sql` with a user-provided parameter. Never run a file uploaded by a user, or a file that is not under your control. + - **recursion**: you can use `run_sql` to include a file that itself includes another file, and so on. However, be careful to avoid infinite loops. SQLPage will throw an error if the inclusion depth is superior to `max_recursion_depth` (10 by default). + - **security**: be careful when using `run_sql` to include files. + - Never use `run_sql` with a user-provided parameter. + - Never run a file uploaded by a user, or a file that is not under your control. + - Remember that users can also run the files you include with `sqlpage.run_sql(...)` directly just by loading the file in the browser. + - Make sure this does not allow users to bypass security measures you put in place such as [access control](/component.sql?component=authentication). + - If you need to include a file, but make it inaccessible to users, you can use hidden files and folders (starting with a `.`), or put files in the special `sqlpage/` folder that is not accessible to users. - **variables**: the included file will have access to the same variables (URL parameters, POST variables, etc.) as the calling file. If the included file changes the value of a variable or creates a new variable, the change will not be visible in the calling file. + +### Parameters + +You can pass parameters to the included file, as if it had been with a URL parameter. +For instance, you can use: + +```sql +sqlpage.run_sql(''included_file.sql'', json_object(''param1'', ''value1'', ''param2'', ''value2'')) +``` + +Which will make `$param1` and `$param2` available in the included file. +[More information about building JSON objects in SQL](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). ' ); INSERT INTO sqlpage_function_parameters ( diff --git a/examples/official-site/sqlpage/migrations/40_fetch.sql b/examples/official-site/sqlpage/migrations/40_fetch.sql index 5dbe6df9..f93d6f50 100644 --- a/examples/official-site/sqlpage/migrations/40_fetch.sql +++ b/examples/official-site/sqlpage/migrations/40_fetch.sql @@ -29,24 +29,27 @@ select $user_search as title, CAST($api_results->>0->>''lat'' AS FLOAT) as latitude, CAST($api_results->>0->>''lon'' AS FLOAT) as longitude; ``` + #### POST query with a body In this example, we use the complex form of the function to make an authenticated POST request, with custom request headers and a custom request body. -We use SQLite''s json functions to build the request body. +We use SQLite''s json functions to build the request body. +See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for +more information on how to build JSON objects in your database. ```sql set request = json_object( - ''method'', ''POST'' + ''method'', ''POST'', ''url'', ''https://postman-echo.com/post'', ''headers'', json_object( ''Content-Type'', ''application/json'', ''Authorization'', ''Bearer '' || sqlpage.environment_variable(''MY_API_TOKEN'') ), ''body'', json_object( - ''Hello'', ''world'', - ), + ''Hello'', ''world'' + ) ); set api_results = sqlpage.fetch($request); @@ -57,15 +60,56 @@ select $api_results as contents; ``` + +#### Authenticated request using Basic Auth + +Here''s how to make a request to an API that requires [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication): + +```sql +set request = json_object( + ''url'', ''https://api.example.com/data'', + ''username'', ''my_username'', + ''password'', ''my_password'' +); +set api_results = sqlpage.fetch($request); +``` + +> This will add the `Authorization: Basic bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQK` header to the request, +> where `bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQK` is the base64 encoding of the string `my_username:my_password`. + # JSON parameter format The fetch function accepts either a URL string, or a JSON object with the following parameters: + - `url`: The URL to fetch. Required. - `method`: The HTTP method to use. Defaults to `GET`. - - `url`: The URL to fetch. - - `headers`: A JSON object with the headers to send. - - `body`: The body of the request. If it is a JSON object, it will be sent as JSON. If it is a string, it will be sent as is. + - `headers`: A JSON object with the headers to send. Defaults to sending a User-Agent header containing the SQLPage version. + - `body`: The body of the request. If it is a JSON object, it will be sent as JSON. If it is a string, it will be sent as is. When omitted, no request body is sent. - `timeout_ms`: The maximum time to wait for the request, in milliseconds. Defaults to 5000. + - `username`: Optional username for HTTP Basic Authentication. Introduced in version 0.33.0. + - `password`: Optional password for HTTP Basic Authentication. Only used if username is provided. Introduced in version 0.33.0. + - `response_encoding`: Optional charset to use for decoding the response body. Defaults to `utf8`, or `base64` if the response contains binary data. All [standard web encodings](https://encoding.spec.whatwg.org/#concept-encoding-get) are supported, plus `hex`, `base64`, and `base64url`. Introduced in version 0.37.0. + +# Error handling and reading response headers + +If the request fails, this function throws an error, that will be displayed to the user. +The response headers are not available for inspection. + +## Conditional data fetching + +Since v0.40, `sqlpage.fetch(null)` returns null instead of throwing an error. +This makes it easier to conditionnally query an API: + +```sql +set current_field_value = (select field from my_table where id = 1); +set target_url = nullif(''http://example.com/api/field/1'', null); -- null if the field is currently null in the db +set api_value = sqlpage.fetch($target_url); -- no http request made if the field is not null in the db +update my_table set field = $api_value where id = 1 and $api_value is not null; -- update the field only if it was not present before +``` + +## Advanced usage +If you need to handle errors or inspect the response headers or the status code, +use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta). ' ); INSERT INTO sqlpage_function_parameters ( diff --git a/examples/official-site/sqlpage/migrations/41_blog_performance.sql b/examples/official-site/sqlpage/migrations/41_blog_performance.sql index 9a0ca8c9..684640d3 100644 --- a/examples/official-site/sqlpage/migrations/41_blog_performance.sql +++ b/examples/official-site/sqlpage/migrations/41_blog_performance.sql @@ -24,7 +24,7 @@ It explains why and how SQLPage applications are often faster than equivalent ap Since SQLPage v0.20.3, SQLPage can natively make requests to external HTTP APIs with [the `fetch` function](/documentation.sql#fetch), which opens the door to many new possibilities. -An example of this is the [**SSO demo**](https://github.com/lovasoa/SQLpage/tree/main/examples/single%20sign%20on), +An example of this is the [**SSO demo**](https://github.com/sqlpage/SQLPage/tree/main/examples/single%20sign%20on), which demonstrates how to use SQLPage to authenticate users on a website using a third-party authentication service, such as Google, Facebook, an enterprise identity provider using [OIDC](https://openid.net/connect/), or an academic institution using [CAS](https://apereo.github.io/cas/). @@ -32,6 +32,6 @@ or an academic institution using [CAS](https://apereo.github.io/cas/). ## New architecture diagram The README of the SQLPage repository now includes a -[clear yet detailed architecture diagram](https://github.com/lovasoa/SQLpage?tab=readme-ov-file#how-it-works). +[clear yet detailed architecture diagram](https://github.com/sqlpage/SQLPage?tab=readme-ov-file#how-it-works). ' ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/42_blog_video.sql b/examples/official-site/sqlpage/migrations/42_blog_video.sql index cb697271..4cd0a09d 100644 --- a/examples/official-site/sqlpage/migrations/42_blog_video.sql +++ b/examples/official-site/sqlpage/migrations/42_blog_video.sql @@ -3,7 +3,7 @@ INSERT INTO blog_posts (title, description, icon, created_at, content) VALUES ( 'Introduction video', - 'A 30-minute live presetation of SQLPage, its raison d''être, and how to use it.', + 'A 30-minute live presentation of SQLPage, its raison d''être, and how to use it.', 'brand-youtube', '2024-05-14', ' @@ -21,7 +21,7 @@ to record a video introduction to SQLPage, which I did. The video is a 5 minute introduction to the philosophy behind SQLPage, followed by a 25 minute live demonstration of how to create -[this todo list application](https://github.com/lovasoa/SQLpage/tree/main/examples/todo%20application) +[this todo list application](https://github.com/sqlpage/SQLPage/tree/main/examples/todo%20application) from scratch. Watch it on youtube: diff --git a/examples/official-site/sqlpage/migrations/45_blog_archeology.sql b/examples/official-site/sqlpage/migrations/45_blog_archeology.sql index 37019a60..14211059 100644 --- a/examples/official-site/sqlpage/migrations/45_blog_archeology.sql +++ b/examples/official-site/sqlpage/migrations/45_blog_archeology.sql @@ -23,7 +23,7 @@ For the archaeologist, excavation is a tool, not an end in itself. What the arch To be exploited, archaeological information must be organized according to well-established principles. The first key concept is the sedimentary layer (*Stratigraphic Unit* - SU), which testifies to a human action or a natural phenomenon. The study of the arrangement of these layers reveals the chronology of the site, the succession of events that took place there. These layers can be grouped into archaeological *facts*: ditches, cellars, burials, are indeed groupings of layers that define a specific element. Finally, the objects found in these layers, or *artifacts*, are cataloged and identified by their layer of origin, providing crucial chronological and cultural indications. -![mastraits site](https://github.com/lovasoa/SQLpage/assets/552629/3dbdf81e-b9d3-4268-a8e3-99e568feb695) +![mastraits site](https://github.com/sqlpage/SQLPage/assets/552629/3dbdf81e-b9d3-4268-a8e3-99e568feb695) *The excavation site of the Necropolis of Mastraits, in Noisy-le-Grand (93).* @@ -46,7 +46,7 @@ The use of electronic tacheometers, then differential GPS, has made it possible The documentary collection obtained at the end of an excavation is particularly precious. These are the only elements that will allow reconstructing the history of the site, by crossing these data with the result of the studies carried out. The fear of the disappearance of this data, or its use by others due to a remarkable discovery, is a feeling often shared within the archaeological community. The archaeologist may feel like a custodian of this information, even expressing a feeling of possession that goes completely against the idea of shared and open science. The idea that opening up data is the best way to protect it is far from obvious. -![conservation sheet, illustrating manual coloring of found skeleton parts](https://github.com/lovasoa/SQLpage/assets/552629/ca9c0f99-a520-4f2b-9826-ae49a89f844b) +![conservation sheet, illustrating manual coloring of found skeleton parts](https://github.com/sqlpage/SQLPage/assets/552629/ca9c0f99-a520-4f2b-9826-ae49a89f844b) > *Conservation sheet, illustrating manual coloring of found skeleton parts* ![Example of a descriptive sheet of an archaeological layer](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/fiche_us.svg) @@ -86,7 +86,7 @@ While the topographer still intervenes for taking reference points, the detailed ![Photogrammetric survey of a burial](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/photogrammetrie3.png) [*Photogrammetric survey of a burial*](https://sketchfab.com/3d-models/973-5d7513dd1dc941228d4a4b7b984c7af7) -Data recording is ensured by the use of a relational and spatial database whose interface is accessible in QGIS, but also via a web interface directly in the field, without going through paper inventories or listings. The web interface was created using [SQLPage](https://sql.ophir.dev/), a web server that uses an SQL-based language for creating the graphical interface, without having to go through more complex programming languages classically used for creating web applications, such as PHP. +Data recording is ensured by the use of a relational and spatial database whose interface is accessible in QGIS, but also via a web interface directly in the field, without going through paper inventories or listings. The web interface was created using [SQLPage](https://sql-page.com/), a web server that uses an SQL-based language for creating the graphical interface, without having to go through more complex programming languages classically used for creating web applications, such as PHP. Of course, this approach also continues in the laboratory during the site analysis stage. @@ -134,7 +134,7 @@ The question of the system''s portability remained. QGIS is a resource-intensive Choosing to use a SpatiaLite or PostGIS database allowed us to consider a web interface from the start, which could then be used on any device. Initially, we considered developing in PHP/HTML/CSS with an Apache web server. However, this required having a web server and programming an entire interface. There were also some infrastructure questions to address: where to host it, how to finance it, and who would manage it all? -It was on LinuxFR that one of the members of the collective discovered [SQLPage](https://sql.ophir.dev/). This open-source software, developed by [lovasoa](https://linuxfr.org/users/lovasoa), provides a very simple web server and allows for the creation of a [CRUD](https://en.wikipedia.org/wiki/CRUD) application with an interface that only requires SQL development. +It was on LinuxFR that one of the members of the collective discovered [SQLPage](https://sql-page.com/). This open-source software, developed by [lovasoa](https://linuxfr.org/users/lovasoa), provides a very simple web server and allows for the creation of a [CRUD](https://en.wikipedia.org/wiki/CRUD) application with an interface that only requires SQL development. SQLPage is based on an executable file which, when launched on a computer, turns it into a web server. A configuration file allows you to define the location of the database to be queried, among other things. For each web page of the interface, you write a `.sql` file to define the data to fetch or modify in the database, and the interface components to display it (tables, forms, graphs...). The interface is accessed through a web browser. If the computer is on a network, its IP address allows remote access, with an address like `http://192.168.1.5:8080`, for example. Using a VPN allows us to use the mobile phone network, eliminating the need for setting up a local network with routers, antennas, etc. @@ -145,7 +145,7 @@ Thus, the installation of the entire system is very simple and relies only on a By relying on the documentation (and occasionally asking questions to the software''s author), we were able to develop a very comprehensive interface on our own that meets our needs in the field. Named Bad''Mobil, the web interface provides access to all the attribute data recorded by archaeologists and now allows, thanks to the constant development of SQLPage, **to visualize spatial data**. Documentation produced during the excavation can also be consulted if the files (photos, scanned drawings, etc.) are placed in the right location in the file structure. The pages mainly consist of creation or modification forms, as well as tables listing already recorded elements. The visualization of geometry allows for spatial orientation in the field, particularly in complex excavation sites, and interaction with attribute data. -[![The BadMobil interface, with SQLPage](https://github.com/lovasoa/SQLpage/assets/552629/b421eebd-1d7a-446a-90d4-f360300453d5)](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/interface_badmobil.webp?ref_type=heads) +[![The BadMobil interface, with SQLPage](https://github.com/sqlpage/SQLPage/assets/552629/b421eebd-1d7a-446a-90d4-f360300453d5)](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/interface_badmobil.webp?ref_type=heads) *The BadMobil interface, with SQLPage* # Use Cases and Concrete Benefits diff --git a/examples/official-site/sqlpage/migrations/47_link.sql b/examples/official-site/sqlpage/migrations/47_link.sql index 495fc90d..267a728d 100644 --- a/examples/official-site/sqlpage/migrations/47_link.sql +++ b/examples/official-site/sqlpage/migrations/47_link.sql @@ -16,25 +16,28 @@ VALUES Let''s say you have a database of products, and you want the main page (`index.sql`) to link to the page of each product (`product.sql`) with the product name as a parameter. -In `index.sql`, you can use the `link` function to generate the URL of the product page for each product: +In `index.sql`, you can use the `link` function to generate the URL of the product page for each product. ```sql select ''list'' as component; select name as title, - sqlpage.link(''product.sql'', json_object(''product_name'', name)) as link; + sqlpage.link(''product'', json_object(''product_name'', name)) as link +from products; ``` -Using `sqlpage.link` is better than manually constructing the URL with `CONCAT(''product.sql?product_name='', name)`, because it ensures that the URL is properly encoded. -The former works when the product name contains special characters like `&`, while the latter would break the URL. - In `product.sql`, you can then use `$product_name` to get the name of the product from the URL parameter: ```sql -select ''text'' as component; -select CONCAT(''Product: '', $product_name) as contents; +select ''hero'' as component, $product_name as title, product_info as description +from products +where name = $product_name; ``` +> You could also have manually constructed the URL with `CONCAT(''product?product_name='', name)`, +> but using `sqlpage.link` is better because it ensures that the URL is properly encoded. +> `sqlpage.link` will work even if the product name contains special characters like `&`, while `CONCAT(...)` would break the URL. + ### Parameters - `file` (TEXT): The name of the SQLPage file to link to. - `parameters` (JSON): The parameters to pass to the linked file. @@ -71,4 +74,4 @@ VALUES 'fragment', 'An optional fragment (hash) to append to the URL to link to a specific section of the target page.', 'TEXT' - ); \ No newline at end of file + ); diff --git a/examples/official-site/sqlpage/migrations/48_status_code.sql b/examples/official-site/sqlpage/migrations/48_status_code.sql new file mode 100644 index 00000000..f80d4266 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/48_status_code.sql @@ -0,0 +1,65 @@ +-- Insert the status_code component into the component table +INSERT INTO + component (name, description, icon) +VALUES + ( + 'status_code', + 'Sets the HTTP response code for the current page. + +This is an advanced technical component. +You typically need it when building internet-facing APIs and websites, +but you may not need it for simple internal applications. + +- Indicating operation results when using [SQLPage as an API](?component=json) + - `200`: *OK*, for successful operations + - `201`: *Created*, for successful record insertion + - `404`: *Not Found*, for missing resources + - `500`: *Internal Server Error*, for failed operations +- Handling data validation errors + - `400`: *Bad Request*, for invalid data +- Enforcing access controls + - `403`: *Forbidden*, for unauthorized access + - `401`: *Unauthorized*, for unauthenticated access +- Tracking system health + - `500`: *Internal Server Error*, for failed operations + +For search engine optimization: +- Use `404` for deleted content to remove outdated URLs from search engines +- For redirection from one page to another, use + - `301` (moved permanently), or + - `302` (moved temporarily) +- Use `503` during maintenance', + 'error-404' + ); + +-- Insert the parameters for the status_code component into the parameter table +INSERT INTO + parameter ( + component, + name, + description, + type, + top_level, + optional + ) +VALUES + ( + 'status_code', + 'status', + 'HTTP status code (e.g., 200 OK, 401 Unauthorized, 409 Conflict)', + 'INTEGER', + TRUE, + FALSE + ); + +INSERT INTO example (component, description) +VALUES ( + 'status_code', + ' +Set the HTTP status code to 404, indicating that the requested resource was not found. +Useful in combination with [`404.sql` files](/your-first-sql-website/custom_urls.sql): + +```sql +SELECT ''status_code'' as component, 404 as status; +``` +'); diff --git a/examples/official-site/sqlpage/migrations/49_big_number.sql b/examples/official-site/sqlpage/migrations/49_big_number.sql new file mode 100644 index 00000000..e18db309 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/49_big_number.sql @@ -0,0 +1,66 @@ +-- Big Number Component Documentation + +-- Component Definition +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('big_number', 'chart-area', 'A component to display key metrics or statistics with optional description, change indicator, and progress bar. Useful in dashboards.', '0.28.0'); + +-- Inserting parameter information for the big_number component +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'big_number', * FROM (VALUES + -- Top-level parameters (for the whole big_number list) + ('columns', 'The number of columns to display the big numbers in (default is one column per item).', 'INTEGER', TRUE, TRUE), + ('id', 'An optional ID to be used as an anchor for links.', 'TEXT', TRUE, TRUE), + ('class', 'An optional CSS class to be added to the component for custom styling', 'TEXT', TRUE, TRUE), + -- Item-level parameters (for each big number) + ('title', 'The title or label for the big number.', 'TEXT', FALSE, TRUE), + ('title_link', 'A link for the Big Number title. If set, the entire title becomes clickable.', 'TEXT', FALSE, TRUE), + ('title_link_new_tab', 'If true, the title link will open in a new tab/window.', 'BOOLEAN', FALSE, TRUE), + ('value_link', 'A link for the Big Number value. If set, the entire value becomes clickable.', 'TEXT', FALSE, TRUE), + ('value_link_new_tab', 'If true, the value link will open in a new tab/window.', 'BOOLEAN', FALSE, TRUE), + ('value', 'The main value to be displayed prominently.', 'TEXT', FALSE, FALSE), + ('unit', 'The unit of measurement for the value.', 'TEXT', FALSE, TRUE), + ('description', 'A description or additional context for the big number.', 'TEXT', FALSE, TRUE), + ('change_percent', 'The percentage change in value (e.g., 7 for 7% increase, -8 for 8% decrease).', 'INTEGER', FALSE, TRUE), + ('progress_percent', 'The value of the progress (0-100).', 'INTEGER', FALSE, TRUE), + ('progress_color', 'The color of the progress bar (e.g., "primary", "success", "danger").', 'TEXT', FALSE, TRUE), + ('dropdown_item', 'A list of JSON objects containing links. e.g. {"label":"This week", "link":"?days=7"}', 'JSON', FALSE, TRUE), + ('color', 'The color of the card', 'COLOR', FALSE, TRUE) +) x; + +INSERT INTO example(component, description, properties) VALUES + ('big_number', 'Big numbers with change indicators and progress bars', + json('[ + {"component":"big_number"}, + { + "title":"Sales", + "value":75, + "unit":"%", + "title_link": "#sales_dashboard", + "title_link_new_tab": true, + "value_link": "#sales_details", + "value_link_new_tab": false, + "description":"Conversion rate", + "change_percent": 9, + "progress_percent": 75, + "progress_color": "blue" + }, + { + "title":"Revenue", + "value":"4,300", + "unit":"$", + "description":"Year on year", + "change_percent": -8 + } + ]')); + +INSERT INTO example(component, description, properties) VALUES + ('big_number', 'Big numbers with dropdowns and customized layout', + json('[ + {"component":"big_number", "columns":3, "id":"colorfull_dashboard"}, + {"title":"Users", "value":"1,234", "color": "red", "title_link": "#users", "title_link_new_tab": false, "value_link": "#users_details", "value_link_new_tab": true }, + {"title":"Orders", "value":56, "color": "green", "title_link": "#orders", "title_link_new_tab": true }, + {"title":"Revenue", "value":"9,876", "unit": "€", "color": "blue", "change_percent": -7, "dropdown_item": [ + {"label":"This week", "link":"?days=7&component=big_number#colorfull_dashboard"}, + {"label":"This month", "link":"?days=30&component=big_number#colorfull_dashboard"}, + {"label":"This quarter", "link":"?days=90&component=big_number#colorfull_dashboard"} + ]} + ]')); diff --git a/examples/official-site/sqlpage/migrations/50_blog_json.sql b/examples/official-site/sqlpage/migrations/50_blog_json.sql new file mode 100644 index 00000000..7e6aa466 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/50_blog_json.sql @@ -0,0 +1,585 @@ +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'JSON in SQL: A Comprehensive Guide', + 'A comprehensive guide to working with JSON data in SQLite, PostgreSQL, MySQL, and SQL Server.', + 'braces', + '2024-09-03', + ' +# JSON in SQL: A Comprehensive Guide + +## Introduction + +JSON (JavaScript Object Notation) is a popular data format for unstructured data. It allows storing composite data types, such as arrays and objects, in a single SQL value. +Many modern applications use JSON to store and exchange data. As a result, SQL databases have incorporated JSON support to allow developers to work with structured and semi-structured data within the same database. + +This guide will cover JSON operations in SQLite, PostgreSQL, MySQL, and SQL Server, focusing on querying JSON data. + +SQLPage uses JSON both to pass data to the database (when a SQLPage variable contains an array), and to pass data to components (when a component has a JSON parameter). +Thus, understanding how to work with JSON in SQL will allow you to fully leverage advanced SQLPage features. + +JSON supports the following data types: + +- **Objects**: A mapping between keys and values (`{ "key": "value" }`). Keys must be strings, and values can be of different types. +- **Arrays**: An ordered list of values enclosed in square brackets (`[ "value1", "value2" ]`). Values can be of different types. +- **Strings**: A sequence of characters enclosed in double quotes (`"Hello, World!"`). +- **Numbers**: An integer or floating-point number (`42`, `3.14`). +- **Boolean**: A true or false value (`true`, `false`). +- **Null**: A null value (`null`). + +## Sample Table + +We''ll use the following sample table for our examples: + +```sql +CREATE TABLE users ( + id INT PRIMARY KEY, + name VARCHAR(50), + birthday DATE, + group_name VARCHAR(50) +); + +INSERT INTO users (id, name, birthday, group_name) VALUES +(1, ''Alice'', ''1990-01-15'', ''Admin''), +(2, ''Bob'', ''1985-05-22'', ''User''), +(3, ''Charlie'', ''1992-09-30'', ''User''); +``` + +## SQLite + +SQLite provides increasingly better JSON support since version 3.38.0. +See [the list of JSON functions in SQLite](https://www.sqlite.org/json1.html) for more details. + +### Creating a JSON object + +We can use the standard `json_object()` function to create a JSON object from columns in a table: + +```sql +SELECT json_object(''name'', name, ''birthday'', birthday) AS user_json +FROM users; +``` + +| user_json | +|-----------| +| `{"name":"Alice","birthday":"1990-01-15"}` | +| `{"name":"Bob","birthday":"1985-05-22"}` | +| `{"name":"Charlie","birthday":"1992-09-30"}` | + +### Creating a JSON array + +```sql +SELECT json_array(name, birthday, group_name) AS user_array +FROM users; +``` + +| user_array | +|------------| +| `["Alice","1990-01-15","Admin"]` | +| `["Bob","1985-05-22","User"]` | +| `["Charlie","1992-09-30","User"]` | + +### Aggregating multiple values into a JSON array + +```sql +SELECT json_group_array(name) AS names +FROM users; +``` + +| names | +|-------| +| `["Alice","Bob","Charlie"]` | + +### Aggregating values into a JSON object + +```sql +SELECT json_group_object(name, group_name) AS name_group_map +FROM users; +``` + +| name_group_map | +|-------------------| +| `{"Alice":"Admin", "Bob":"User", "Charlie":"User"}` | + + +### Iterating over a JSON array + +SQLite provides the `json_each()` table-valued function to iterate over JSON arrays. This function returns one row for each element in the JSON array. + +```sql +SELECT value FROM json_each(''["Alice", "Bob", "Charlie"]''); +``` + +| value | +|-------| +| Alice | +| Bob | +| Charlie | + +The `json_each()` function returns a table with several columns. The most commonly used are: + +- `key`: The array index (0-based) for elements of a JSON array +- `value`: The value of the current element +- `type`: The type of the current element (e.g., ''text'', ''integer'', ''real'', ''true'', ''false'', ''null'') + +For more complex JSON structures, you can use the `json_tree()` function, which recursively walks through the entire JSON structure. + +These iteration functions can be used to check if specific values exist in a JSON array. +Here''s a practical example: +Let''s say you have a form with a [multiple-choice dropdown](documentation.sql?component=form#component) that allows selecting multiple users. +Some users might already be selected, and their IDs are stored in a JSON array passed as an URL parameter called `$selected_ids`. +You can create this dropdown using the following query: + +```sql +select json_group_array(json_object( + ''label'', name, + ''value'', id, + ''selected'', id in (select value from json_each_text($selected_ids)) +)) as options +from users; +``` + +This query will: +1. Create a dropdown option for each user +2. Use their name as the display label +3. Use their ID as the value +4. Mark the option as selected if the user''s ID exists in the $selected_ids array + +### Combining two JSON objects + +SQLite provides the `json_patch()` function to combine two JSON objects. This function takes two JSON objects as arguments and returns a new JSON object that is the result of merging the two input objects. + +```sql +SELECT json_patch(''{"name": "Alice"}'', ''{"birthday": "1990-01-15"}'') AS user_json; +``` + +| user_json | +|-----------| +| {"name": "Alice", "birthday": "1990-01-15"} | + +## PostgreSQL + +PostgreSQL has extensive support for JSON, including the `jsonb` type, which offers better performance and more functionality than the `json` type. +See [the list of JSON functions in PostgreSQL](https://www.postgresql.org/docs/current/functions-json.html) for more details. + +### Creating a JSON object + +```sql +SELECT + jsonb_build_object( + ''name'', name, + ''birthday'', birthday + ) AS user_json +FROM users; +``` + +| user_json | +|-----------| +| `{"name":"Alice","birthday":"1990-01-15"}` | +| `{"name":"Bob","birthday":"1985-05-22"}` | +| `{"name":"Charlie","birthday":"1992-09-30"}` | + +### Creating a JSON array + +```sql +SELECT + jsonb_build_array( + name, birthday, group_name + ) AS user_array +FROM users; +``` + +| user_array | +|------------| +| `["Alice", "1990-01-15", "Admin"]` | +| `["Bob", "1985-05-22", "User"]` | +| `["Charlie", "1992-09-30", "User"]` | + +### Aggregating multiple values into a JSON array + +```sql +SELECT jsonb_agg(name) AS names FROM users; +``` + +| names | +|-------| +| `["Alice","Bob","Charlie"]` | + +### Aggregating values into a JSON object + +```sql +SELECT + jsonb_object_agg( + name, birthday + ) AS name_birthday_map +FROM users; +``` + +| name_birthday_map | +|-------------------| +| `{"Alice":"1990-01-15","Bob":"1985-05-22","Charlie":"1992-09-30"}` | + + +### Iterating over a JSON array + +```sql +SELECT name FROM jsonb_array_elements_text(''["Alice", "Bob", "Charlie"]''::jsonb) AS name; +``` + +| name | +|------| +| Alice | +| Bob | +| Charlie | + +You can use this function to test whether a value is present in a JSON array. For instance, to create a +[multi-value select dropdown](documentation.sql?component=form#component) with pre-selected values, you can use the following query: + +```sql +SELECT jsonb_agg(jsonb_build_object( + ''label'', name, + ''value'', id, + ''selected'', id in (SELECT value FROM jsonb_array_elements_text($selected_ids::jsonb)) +)) AS options +FROM users; +``` + +### Iterating over a JSON object + +```sql +SELECT key, value +FROM jsonb_each_text(''{"name": "Alice", "birthday": "1990-01-15"}''::jsonb); +``` + +| key | value | +|-----|-------| +| name | Alice | +| birthday | 1990-01-15 | + +### Querying JSON data + +PostgreSQL allows you to query JSON data using the `->` and `->>` operators: + +```sql +SELECT name, user_data->>''age'' AS age +FROM ( + SELECT name, jsonb_build_object(''age'', EXTRACT(YEAR FROM AGE(birthday))) AS user_data + FROM users +) subquery +WHERE (user_data->>''age'')::int > 30; +``` + +| name | age | +|------|-----| +| Bob | 38 | + +### Combining two JSON objects + +PostgreSQL provides the `||` operator to combine two JSON objects. + +```sql +SELECT ''{"name": "Alice"}''::jsonb || ''{"birthday": "1990-01-15"}''::jsonb AS user_json; +``` + +| user_json | +|-----------| +| {"name": "Alice", "birthday": "1990-01-15"} | + +## MySQL / MariaDB + +MySQL has good support for JSON operations starting from version 5.7. +See [the list of JSON functions in MySQL](https://dev.mysql.com/doc/refman/8.0/en/json-functions.html) for more details. + +### Creating a JSON object + +```sql +SELECT JSON_OBJECT(''name'', name, ''birthday'', birthday) AS user_json +FROM users; +``` + +| user_json | +|-----------| +| `{"name":"Alice","birthday":"1990-01-15"}` | +| `{"name":"Bob","birthday":"1985-05-22"}` | +| `{"name":"Charlie","birthday":"1992-09-30"}` | + +### Creating a JSON array + +```sql +SELECT JSON_ARRAY(name, birthday, group_name) AS user_array +FROM users; +``` + +| user_array | +|------------| +| `["Alice","1990-01-15","Admin"]` | +| `["Bob","1985-05-22","User"]` | +| `["Charlie","1992-09-30","User"]` | + +### Aggregating multiple values into a JSON array + +```sql +SELECT JSON_ARRAYAGG(name) AS names +FROM users; +``` + +| names | +|-------| +| `["Alice","Bob","Charlie"]` | + +### Aggregating values into a JSON object + +```sql +SELECT JSON_OBJECTAGG(name, birthday) AS name_birthday_map +FROM users; +``` + +| name_birthday_map | +|-------------------| +| `{"Alice":"1990-01-15","Bob":"1985-05-22","Charlie":"1992-09-30"}` | + +### Iterating over a JSON array + +MySQL provides the JSON_TABLE() function to iterate over JSON arrays. This powerful function allows you to convert JSON data into a relational table format, making it easy to work with JSON arrays. + +Here''s an example of how to use JSON_TABLE() to iterate over a JSON array: + +```sql +SELECT jt.name +FROM JSON_TABLE( + ''["Alice", "Bob", "Charlie"]'', + ''$[*]'' COLUMNS( name VARCHAR(50) PATH ''$'' ) +) AS jt; +``` + +| name | +|---------| +| Alice | +| Bob | +| Charlie | + +In this example: +- The first argument to JSON_TABLE() is the JSON array. +- `''$[*]''` is the path expression that selects all elements of the array. +- The `COLUMNS` clause defines the structure of the output table. In our case, we want a single column named `name`: + - `name VARCHAR(50) PATH ''$''` creates a text column that contains the raw value of each array element in its entirety (`$` is the current element). + +You can also use JSON_TABLE() with more complex JSON structures: + +```sql +SELECT jt.* +FROM JSON_TABLE( + ''[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Charlie"}]'', + ''$[*]'' COLUMNS( + id INT PATH ''$.id'', + name VARCHAR(50) PATH ''$.name'' + ) +) AS jt; +``` + +| id | name | +|----|---------| +| 1 | Alice | +| 2 | Bob | +| 3 | Charlie | + +This approach allows you to easily iterate over JSON arrays and access their elements in a tabular format, which can be very useful for further processing or joining with other tables in your database. + +### Iterating over a JSON object + +The `JSON_TABLE` function can also be used to iterate over JSON objects: + +```sql +SELECT jt.* +FROM JSON_TABLE( + ''{"name": "Alice", "birthday": "1990-01-15"}'', + ''$.*'' COLUMNS ( + value JSON PATH ''$'' + ) +) AS jt; +``` + +| value | +|-------| +| "Alice" | +| "1990-01-15" | + +#### Iterating over key-value pairs + +You can use the `JSON_KEYS()` function to retrieve the list of keys in a JSON object as a JSON array, +then use that array to iterate over the keys of a JSON object: + +```sql +SELECT json_key, json_extract(json_str, CONCAT(''$.'', json_key)) as json_value +FROM + (select ''{"name": "Alice", "birthday": "1990-01-15"}'' as json_str) AS my_json, + JSON_TABLE(json_keys(json_str), ''$[*]'' COLUMNS (json_key JSON PATH ''$'')) AS json_keys; +``` + +| json_key | json_value | +|----------|------------| +| name | Alice | +| birthday | 1990-01-15 | + +### Querying JSON data + +MySQL allows you to query JSON data using the `->` and `->>` operators: + +```sql +SELECT name, user_data->''$.age'' AS age +FROM ( + SELECT name, JSON_OBJECT(''age'', YEAR(CURDATE()) - YEAR(birthday)) AS user_data + FROM users +) subquery +WHERE user_data->''$.age'' > 30; +``` + +| name | age | +|------|-----| +| Bob | 38 | + +## Microsoft SQL Server + +SQL Server has support for JSON operations starting from SQL Server 2016. +See [the list of JSON functions in SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-functions-transact-sql?view=sql-server-ver16) for more details. + +# JSON in SQL: A Comprehensive Guide + +[Previous sections remain unchanged] + +## Microsoft SQL Server + +SQL Server has support for JSON operations starting from SQL Server 2016. It provides a comprehensive set of functions for working with JSON data. +See [the list of JSON functions in SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-functions-transact-sql?view=sql-server-ver16) for more details. + +### Creating a JSON object + +Use the `FOR JSON PATH` clause to create a JSON object: + +```sql +SELECT (SELECT name, birthday FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS user_json +FROM users; +``` + +| user_json | +|-----------| +| `{"name":"Alice","birthday":"1990-01-15"}` | +| `{"name":"Bob","birthday":"1985-05-22"}` | +| `{"name":"Charlie","birthday":"1992-09-30"}` | + +Alternatively, you can use the `JSON_OBJECT` function: + +```sql +SELECT JSON_OBJECT(''name'': name, ''birthday'': birthday) AS user_json +FROM users; +``` + +### Creating a JSON array + +Use the `FOR JSON PATH` clause to create a JSON array: + +```sql +SELECT (SELECT name, birthday, group_name FOR JSON PATH) AS user_array +FROM users; +``` + +| user_array | +|------------| +| `[{"name":"Alice","birthday":"1990-01-15","group_name":"Admin"}]` | +| `[{"name":"Bob","birthday":"1985-05-22","group_name":"User"}]` | +| `[{"name":"Charlie","birthday":"1992-09-30","group_name":"User"}]` | + +You can also use the `JSON_ARRAY` function: + +```sql +SELECT JSON_ARRAY(name, birthday, group_name) AS user_array +FROM users; +``` + +### Aggregating multiple values into a JSON array + +Use the `FOR JSON PATH` clause to aggregate values into a JSON array: + +```sql +SELECT (SELECT name FROM users FOR JSON PATH) AS names; +``` + +| names | +|-------| +| `[{"name":"Alice"},{"name":"Bob"},{"name":"Charlie"}]` | + +Alternatively, use the `JSON_ARRAYAGG` function: + +```sql +SELECT JSON_ARRAYAGG(name) AS names FROM users; +``` + +### Aggregating values into a JSON object + +```sql +SELECT JSON_OBJECTAGG(name: birthday) AS name_birthday_map FROM users; +``` + +### Iterating over a JSON array + +Use the `OPENJSON` function to iterate over JSON arrays: + +```sql +SELECT value FROM OPENJSON(''["Alice", "Bob", "Charlie"]''); +``` + +| value | +|-------| +| Alice | +| Bob | +| Charlie | + +### Iterating over a JSON object + +Use `OPENJSON` to iterate over JSON objects: + +```sql +SELECT * +FROM OPENJSON(''{"name": "Alice", "birthday": "1990-01-15"}'') +WITH ( + name NVARCHAR(50) ''$.name'', + birthday DATE ''$.birthday'' +); +``` + +| name | birthday | +|------|----------| +| Alice | 1990-01-15 | + +### Querying JSON data + +Use the `JSON_VALUE` function to extract scalar values from JSON: + +```sql +SELECT JSON_VALUE(''{"age": 38}'', ''$.age'') AS age +``` + +| age | +|-----| +| 38 | + +### Additional JSON Functions + +SQL Server provides several other useful JSON functions: + +- `ISJSON`: Tests whether a string contains valid JSON. +- `JSON_MODIFY`: Updates the value of a property in a JSON string. +- `JSON_PATH_EXISTS`: Tests whether a specified SQL/JSON path exists in the input JSON string. +- `JSON_QUERY`: Extracts an object or an array from a JSON string. + +Example using `JSON_MODIFY`: + +```sql +SELECT JSON_MODIFY(''{"name": "Alice", "age": 30}'', ''$.age'', 31) AS updated_json; +``` + +| updated_json | +|--------------| +| {"name": "Alice", "age": 31} | + +This comprehensive guide covers the basics of working with JSON in SQLite, PostgreSQL, MySQL, and SQL Server. Each database has its own set of functions and syntax for JSON operations, but the general concepts remain similar across all platforms. +'); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/51_column.sql b/examples/official-site/sqlpage/migrations/51_column.sql new file mode 100644 index 00000000..06674e93 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/51_column.sql @@ -0,0 +1,118 @@ +-- Column Component Documentation + +-- Component Definition +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('columns', 'columns', 'A component to display various items in a card layout, allowing users to choose options. Useful for showcasing different features or services, or KPIs. See also the big_number component.', '0.29.0'); + +-- Inserting parameter information for the column component +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'columns', * FROM (VALUES + ('title', 'The title or label for the item.', 'TEXT', FALSE, TRUE), + ('value', 'The value associated with the item.', 'TEXT', FALSE, TRUE), + ('description', 'A brief description of the item.', 'TEXT', FALSE, TRUE), + ('description_md', 'A brief description of the item, formatted using markdown.', 'TEXT', FALSE, TRUE), + ('item', 'A list of bullet points associated with the columns, represented either as text, or as a json object with "icon", "color", and "description" or "description_md" fields.', 'JSON', FALSE, TRUE), + ('link', 'A link associated with the item.', 'TEXT', FALSE, TRUE), + ('button_text', 'Text for the button.', 'TEXT', FALSE, TRUE), + ('button_color', 'Optional color for the button.', 'TEXT', FALSE, TRUE), + ('target', 'Optional target for the button. Set to "_blank" to open links in a new tab.', 'TEXT', FALSE, TRUE), + ('value_color', 'Color for the value text.', 'TEXT', FALSE, TRUE), + ('small_text', 'Optional small text to display after the value.', 'TEXT', FALSE, TRUE), + ('icon', 'Optional icon to display in a ribbon.', 'ICON', FALSE, TRUE), + ('icon_color', 'Color for the icon in the ribbon.', 'TEXT', FALSE, TRUE), + ('size', 'Size of the column, affecting layout.', 'INTEGER', FALSE, TRUE) +) x; + +INSERT INTO example(component, description, properties) VALUES + ('columns', 'Pricing Plans Display', + json('[ + {"component":"columns"}, + { + "title":"Start Plan", + "value":"€18", + "description":"Perfect for testing and small-scale projects", + "item": [ + "128MB Database", + "SQLPage hosting", + "Community support" + ], + "link":"https://datapage.app", + "button_text":"Start Free Trial", + "small_text":"/month" + }, + { + "title":"Pro Plan", + "value":"€40", + "icon":"rocket", + "description":"For growing projects needing enhanced features", + "item": [ + {"icon":"database", "color": "blue", "description":"1GB Database"}, + {"icon":"headset", "color": "green", "description":"Priority Support"}, + {"icon":"world", "color": "purple", "description":"Custom Domain"} + ], + "link":"https://datapage.app", + "button_text":"Start Free Trial", + "button_color":"indigo", + "value_color":"indigo", + "small_text":"/month" + }, + { + "title":"Enterprise Plan", + "value":"€600", + "icon":"building-skyscraper", + "description":"For large-scale operations with custom needs", + "item": [ + {"icon":"database-plus", "description_md":"**Custom Database Scaling**"}, + {"icon":"shield-lock", "description_md":"**Enterprise Auth** with Single Sign-On"}, + {"icon":"headset", "description_md":"**Monthly** Expert Support time"}, + {"icon":"file-certificate", "description_md":"**SLA** with guaranteed uptime"} + ], + "link":"mailto:contact@datapage.app", + "button_text":"Contact Us", + "small_text":"/month", + "target":"_blank" + } + ]')), + + ('columns', 'Tech Company KPIs Display', + json('[ + {"component":"columns"}, + { + "title":"Monthly Active Users", + "value":"10k", + "value_color":"blue", + "size": 4, + "description":"Total active users this month, showcasing user engagement.", + "item": [ + {"icon": "target", "description":"Target: 12,000"} + ], + "link":"#", + "button_text":"User Activity Overview", + "button_color":"info" + }, + { + "title":"Revenue", + "value":"$49k", + "value_color":"blue", + "size": 4, + "description":"Total revenue generated this month, indicating financial performance.", + "item": [ + {"icon":"trending-down", "color": "red", "description":"down from $51k last month" } + ], + "link":"#", + "button_text":"Financial Dashboard", + "button_color":"info" + }, + { + "title":"Customer Satisfaction", + "value":"94%", + "value_color":"blue", + "size": 4, + "description":"Percentage of satisfied customers, reflecting service quality.", + "item": [ + {"icon":"trending-up", "color": "green", "description":"+ 2% this month" } + ], + "link":"#", + "button_text": "Open Google Ratings", + "button_color":"info" + } + ]')); diff --git a/examples/official-site/sqlpage/migrations/52_foldable.sql b/examples/official-site/sqlpage/migrations/52_foldable.sql new file mode 100644 index 00000000..d70201df --- /dev/null +++ b/examples/official-site/sqlpage/migrations/52_foldable.sql @@ -0,0 +1,27 @@ +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('foldable', 'chevrons-down', 'A foldable list of elements which can be expanded individually.', '0.29.0'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'foldable', * FROM (VALUES + ('id', 'ID attribute added to the container in HTML. Used for targeting through CSS or for scrolling via links. When set at the top level, applies to the entire foldable component.', 'TEXT', TRUE, TRUE), + ('class', 'CSS class(es) to add to the foldable container. When set at the top level, applies to the entire foldable component.', 'TEXT', TRUE, TRUE), + ('id', 'ID attribute added to individual foldable items. Used for targeting through CSS or for scrolling via links.', 'TEXT', FALSE, TRUE), + ('class', 'CSS class(es) to add to individual foldable items.', 'TEXT', FALSE, TRUE), + ('title', 'Title of the foldable item, displayed on the button.', 'TEXT', FALSE, TRUE), + ('description', 'Plain text description of the item, displayed when expanded.', 'TEXT', FALSE, TRUE), + ('description_md', 'Markdown description of the item, displayed when expanded.', 'TEXT', FALSE, TRUE), + ('expanded', 'If set to TRUE, the foldable item starts in an expanded state. Defaults FALSE', 'BOOLEAN', FALSE, TRUE) +) x; + +INSERT INTO example(component, description, properties) VALUES + ('foldable', 'A single foldable paragraph of text', json('[ + {"component":"foldable"}, + {"title":"The foldable component", "description": "This is a simple foldable component. It can be used to show and hide content. It is a list of items, each with a title and a description. The description is displayed when the item is expanded."}, + ]')); + +INSERT INTO example(component, description, properties) VALUES + ('foldable', 'A SQLPage-themed foldable list with Markdown', json('[ + {"component":"foldable"}, + {"title":"Quick Prototyping", "description_md": "Build a functional web app prototype in minutes using just SQL queries:\n\n- Rapid development\n- Ideal for MVPs\n- Great for internal tools\n\nLearn more about [quick prototyping](/your-first-sql-website/).", "expanded": true}, + {"title":"Data Visualization", "description_md": "Quickly transform your database into useful insights:\n\n1. **Charts**: Line, bar, pie\n2. **KPIs**: Appealing visualizations of key metrics\n3. **Maps**: Geospatial data\n\nAs simple as:\n\n```sql\nSELECT ''chart'' as component;\nSELECT date as x, revenue as y FROM sales;\n```"}, + {"title":"Don''t stare, interact!", "description_md": "SQLPage is not just a passive *Business Intelligence* tool. With SQLPage, you can act upon user input:\n\n- *User input collection*: Building a form is just as easy as building a chart.\n- *Data validation*: Write your own validation rules in SQL.\n- *Database updates*: `INSERT` and `UPDATE` are first-class citizens.\n- *File uploads*: Upload `CSV` and other files, store and display them the way you want.\n\n> Let users interact with your data, not just look at it!"} + ]')); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/53_blog_pgconf2024.sql b/examples/official-site/sqlpage/migrations/53_blog_pgconf2024.sql new file mode 100644 index 00000000..1f859c28 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/53_blog_pgconf2024.sql @@ -0,0 +1,28 @@ + +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'Come and see us in Athens !', + 'PGConf 2024 is coming to Athens, Greece. And we will be there to talk about SQLPage & Archeology !', + 'microphone-2', + '2024-10-15', + ' +# PostgreSQL & SQLPage at PGConf 2024 + +We have been invited to give a talk at PGConf 2024 in Athens, Greece. + +Last year, we gave a general introduction to SQLPage at PGConf.eu 2023 in Prague. + +This year, we will focus on a more specific use case: +using SQLPage to build a power-tool for archaeologists to explore and understand archaeological sites, +radically changing the way archaeologists work, and allowing them to +drastically reduce the time spent on data entry and management. + +There will be two presenters: + - **Ophir Lojkine**, the creator and maintainer of SQLPage, will present the project and its capabilities. + - **Thomas Guillemard**, an archaeologist from France''s National Institute for Preventive Archaeological Research, will present the project''s use case and its benefits. + +Come and see us in Athens ! + +https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5707-unearthing-the-past-with-postgresql-how-open-source-is-revolutionizing-digital-archaeology/ +'); diff --git a/examples/official-site/sqlpage/migrations/54_blog_bompard.sql b/examples/official-site/sqlpage/migrations/54_blog_bompard.sql new file mode 100644 index 00000000..4fc44573 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/54_blog_bompard.sql @@ -0,0 +1,141 @@ + +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'How I built a 360° performance monitoring tool with SQL Queries', + 'Alexis built a performance monitoring tool with SQLPage for a 100M€/year company', + 'shirt', + '2025-01-25', + ' +# How I Built And Deployed An Exhaustive Performance Monitoring Tool For a 100M€/year Company Using SQL Queries Only + +### What is SQLPage ? + +> [SQLPage](http://sql-page.com) allows anyone with SQL skills to build and deploy digital tools (websites, data applications, dashboards, user forms…) using only **SQL queries**. Official website: [https://sql-page.com/](https://sql-page.com/) + +SQLPage eliminates the need to learn server languages, HTML, CSS, JavaScript, or front-end frameworks, and instead uses SQL to generate modern UI layouts populated with database query results. You get native SQL interactions with the database, without all the other layers that typically get in the way. + +The execution of the project is straightforward: simply run a single executable without any installation dependencies. Everything from authentication to security, and even HTTPS termination is automated. The code required to complete most real-world development tasks is minimal and seamless. + +Finally, it’s open source with an MIT license. + +### Why SQLPage became a game-changer for me, as a Head of Data + +As a Head of Data at a mid-size company, I understand the challenge of juggling multiple tools — often expensive and proprietary — alongside a variety of dashboards. Building an **all-in-one**, **user-friendly**, **mobile-compatible** platform for data monitoring and visualization that serves everyone, from C-level executives to store managers, is no small feat. + +The struggle intensifies when teams are small and lack coding skills or experience with diverse tech stacks. A typical data flow in a digital-native company involves several teams, specialized skills, and costly tools: + +![](https://cdn-images-1.medium.com/max/800/1*1IoXc8-07rqXO3yvKC13nQ.png) + +*Typical Data Flow of digital native companies. + +SQLPage changes this by allowing data professionals to use the same language — SQL — across the entire process, from building data pipelines to creating fully functional digital tools. Data analysts, scientists, business analysts, DBAs, and IT teams already have the expertise to craft their own custom data applications from the ground up. + +### Building an all-in one monitoring tool using SQL-queries only +[![youtube](https://github.com/user-attachments/assets/1afe36d7-9deb-40fc-a174-7a869348500b)](https://www.youtube.com/embed/R-5Pej8Sw18?si=qgxacwip2Mm-0wC7) + +*Excerpt from a series of videos explaining how to build and deploy your first digital tool with SQLPage* ([https://www.youtube.com/@SQLPage](https://www.youtube.com/@SQLPage)). + + +I am using SQLPage to build a 360° Performance tool for my company, integrating data from multiple sources — Revenue, traffic, marketing investments, live performance monitoring, financial targets, images of top sold products, Google Analytics for the online traffic… — . + +#### With SQLPage, I can: + +- **Centralize** all company data in one tool for visualizations, year-over-year comparisons, financial targets, and more. +- **Provide tailored insights**: A store owner can instantly access last year’s performance and top-selling products, while the e-commerce director can track conversion rate history. SQLPage’s pre-built components offer limitless possibilities for displaying results. +- **Perform CRUD operations**: Unlike traditional BI tools, SQLPage not only displays data but also allows users to interact with it — inputting data, such as comments or updates, directly through the interface. This capability to both display and collect data is a significant enhancement over traditional BI tools, which typically do not support data input. +- **Ensure a single source of truth**: By connecting directly to the database, SQLPage avoids discrepancies between dashboards, ensuring all teams work with consistent and accurate data. + +Here are some pages I built using only SQL queries, allowing everyone in the company to instantly access any level of information, from the fiscal year 2024 revenue trends to the top-selling products in Marseille in October 2022. + +![](https://cdn-images-1.medium.com/max/800/1*MkORbAC7oGEG-8I1mthu6A.png) + +*Performance of different channels vs last year and best sellers. + +![](https://cdn-images-1.medium.com/max/800/1*_3-g1om_p9ghXhdcw0zHmw.png) + +*Examples of views built with SQLPage to provide a 360° tool for the company. + +### How Does It Work ? + +The process in SQLPage follows a simple pattern: + +> 1) Select a component + +> 2) Write a query to populate the selected component with data + +You can find the full list of components: [https://sql-page.com/documentation.sql](https://sql-page.com/documentation.sql) + +Here’s an example of a parameterized SQL query that uses the “chart” component, along with the query to feed data into it: + + + +```sql +-- Chart Component +SELECT ''chart'' AS component, + CONCAT(''Daily Revenue from '', $start_date_comparison, '' to '', $end_date_comparison) AS title, + ''area'' AS type, + ''indigo'' AS color, + 5 AS marker, + 0 AS ymin; + +-- Chart Data Query +SELECT DATE(business_date) AS x, + ROUND(SUM(value), 2) AS y +FROM data_example +WHERE DATE(business_date) BETWEEN $start_date_comparison AND $end_date_comparison + AND variable_name = ''CA'' +GROUP BY DATE(business_date) +ORDER BY x ASC; + +-- NB: The variables $start_date_comparison and $end_date_comparison are +-- defined dynamically in the SQL script +``` +And the result: + +![Example of a SQL-generated page using the “graph” and the “table” components](https://cdn-images-1.medium.com/max/800/1*mtgJNP7DSOmMnq0iu6dDcg.png) + +*Example of a SQL-generated page using the components “graphs” and “tables”.* + +That’s it! Each component comes with customizable parameters, allowing you to tailor the display. As shown in the screenshot, links are clickable, enabling users to add data, such as leaving a comment for a specific date. + +The ability to perform CRUD operations and interact directly with databases is a game changer compared to traditional BI tools. You can try it yourself by clicking “add” in the column “COMMENT THIS YEAR” [https://demo-test.datapage.app/lets_see_some_graphs.sql](https://demo-test.datapage.app/lets_see_some_graphs.sql) + +### What About GenAI ? + +I couldn’t write an article about data in 2024 without mentioning GenAI. The great news is that SQLPage, relying solely on SQL queries, is naturally GenAI-friendly. In fact, I rarely write SQL queries myself anymore — I let GenAI handle that. My workflow in SQLPage now becomes: + +> 1) Select a component + +> 2) Ask a GenAI tool to write the query I need + +![](https://cdn-images-1.medium.com/max/800/1*mg45EO7XCVPNiuIQ_5Xg0Q.png) + +*Example of Generated SQL to display a specific format of numbers.* + +### How to Host Your SQLPage Application + +Once my app was ready, I could have chosen to host it myself on any server for a few euros a month, but I opted for SQLPage’s official hosting service, DataPage ([https://datapage.app/](https://beta.datapage.app/)), which is fully managed and very convenient. My app was hosted at _domainname.datapage.app._ The service includes a Postgres database, allowing you to either store your data on the server or connect directly to your existing database (Microsoft SQL Server, SQLite, Postgres, MySQL, etc). + +### What Difficulties Can Be Encountered With SQLPage + +While SQLPage simplifies the process of building digital tools, it does come with some challenges. + +As applications grow in complexity, so do the SQL queries required to power them, which can result in long and intricate scripts. Additionally, to fully leverage SQLPage, you need to understand how its components work, especially if user input is involved. Developers should be comfortable with creating tables in a database, writing `INSERT` queries, and managing data effectively. Without a solid grasp of these fundamentals, building more advanced apps can become a bit overwhelming. + +### Conclusion + +With SQLPage, any company with a database and one employee who knows how to query it has the tools and workforce to build and deploy virtually any digital tool. + +In this article, I focused on creating an enhanced Business Intelligence tool, but SQLPage’s versatility goes far beyond that. It is being used to build a planning tool for lumberjacks in Finland, a monitoring app for a South African transport and logistics company, by archaeologists to input excavation data in the field… + +What all these projects have in common is that they were built by a single person, using nothing but SQL queries. If you’re ready to streamline your processes and build powerful tools with ease, SQLPage is worth exploring further. + +### Useful links + +- 🏡Official website [https://sql-page.com](https://sql-page.com/) +- 🔰Quick start (written by [Nick Antonaccio](https://medium.com/u/b6a791990395)): [https://learnsqlpage.com/sqlpage_quickstart.html](https://learnsqlpage.com/sqlpage_quickstart.html) +- 📹Youtube tutorial videos on SQLPage channel: [https://www.youtube.com/@SQLPage/playlists](https://www.youtube.com/@SQLPage/playlists) +- 🤓github: [https://github.com/sqlpage/SQLPage/](https://github.com/sqlpage/SQLPage/) +- ☁️Host your applications: [https://datapage.app](https://datapage.app) +'); diff --git a/examples/official-site/sqlpage/migrations/55_request_body.sql b/examples/official-site/sqlpage/migrations/55_request_body.sql new file mode 100644 index 00000000..d8dd1c94 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/55_request_body.sql @@ -0,0 +1,135 @@ +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'request_body', + '0.33.0', + 'http-post', + 'Returns the raw request body as a string. + +A client (like a web browser, mobile app, or another server) can send information to your server in the request body. +This function allows you to read that information in your SQL code, +in order to create or update a resource in your database for instance. + +The request body is commonly used when building **REST APIs** (machines-to-machines interfaces) +that receive data from the client. + +This is especially useful in: +- `POST` and `PUT` requests for creating or updating resources in your database +- Any API endpoint that needs to receive complex data + +### Example: Building a REST API + +Here''s an example of building an API endpoint that receives a json object, +and inserts it into a database. + +#### `api/create_user.sql` +```sql +-- Get the raw JSON body +set user_data = sqlpage.request_body(); + +-- Insert the user into database +with parsed_data as ( + select + json_extract($user_data, ''$.name'') as name, + json_extract($user_data, ''$.email'') as email +) +insert into users (name, email) +select name, email from parsed_data; + +-- Return success response +select ''json'' as component, + json_object( + ''status'', ''success'', + ''message'', ''User created successfully'' + ) as contents; +``` + +### Testing the API + +You can test this API using curl: +```bash +curl -X POST http://localhost:8080/api/create_user \ + -H "Content-Type: application/json" \ + -d ''{"name": "John", "email": "john@example.com"}'' +``` + +## Special cases + +### NULL + +This function returns NULL if: + - There is no request body + - The request content type is `application/x-www-form-urlencoded` or `multipart/form-data` + (in these cases, use [`sqlpage.variables(''post'')`](?function=variables) instead) + +### Binary data + +If the request body is not valid text encoded in UTF-8, +invalid characters are replaced with the Unicode replacement character `�` (U+FFFD). + +If you need to handle binary data, +use [`sqlpage.request_body_base64()`](?function=request_body_base64) instead. +' + ); + +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'request_body_base64', + '0.33.0', + 'photo-up', + 'Returns the raw request body encoded in base64. This is useful when receiving binary data or when you need to handle non-text content in your API endpoints. + +### What is Base64? + +Base64 is a way to encode binary data (like images or files) into text that can be safely stored and transmitted. This function automatically converts the incoming request body into this format. + +### Example: Handling Binary Data in an API + +This example shows how to receive and process an image uploaded directly in the request body: + +```sql +-- Assuming this is api/upload_image.sql +-- Client would send a POST request with the raw image data + +-- Get the base64-encoded image data +set image_data = sqlpage.request_body_base64(); + +-- Store the image data in the database +insert into images (data, uploaded_at) +values ($image_data, current_timestamp); + +-- Return success response +select ''json'' as component, + json_object( + ''status'', ''success'', + ''message'', ''Image uploaded successfully'' + ) as contents; +``` + +You can test this API using curl: +```bash +curl -X POST http://localhost:8080/api/upload_image.sql \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@/path/to/image.jpg" +``` + +This is particularly useful when: +- Working with binary data (images, files, etc.) +- The request body contains non-UTF8 characters +- You need to pass the raw body to another system that expects base64 + +> Note: Like [`sqlpage.request_body()`](?function=request_body), this function returns NULL if: +> - There is no request body +> - The request content type is `application/x-www-form-urlencoded` or `multipart/form-data` +> (in these cases, use [`sqlpage.variables(''post'')`](?function=variables) instead) +' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/56_headers.sql b/examples/official-site/sqlpage/migrations/56_headers.sql new file mode 100644 index 00000000..b88aaf4e --- /dev/null +++ b/examples/official-site/sqlpage/migrations/56_headers.sql @@ -0,0 +1,39 @@ +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'headers', + '0.33.0', + 'circle-dotted-letter-h', + 'Returns all HTTP request headers as a JSON object. + +### Example + +The following displays all HTTP request headers in a list, +using SQLite''s `json_each()` function. + +```sql +select ''list'' as component; + +select key as title, value as description +from json_each(sqlpage.headers()); -- json_each() is SQLite only +``` + +If not on SQLite, use your [database''s JSON function](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). + +### Details + +The function returns a JSON object where: +- Keys are lowercase header names +- Values are the corresponding header values +- If no headers are present, returns an empty JSON object `{}` + +This is useful when you need to: +- Debug HTTP requests +- Access multiple headers at once + +If you only need access to a single known header, use [`sqlpage.header(name)`](?function=header) instead. +'); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/57_client_ip.sql b/examples/official-site/sqlpage/migrations/57_client_ip.sql new file mode 100644 index 00000000..168451f4 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/57_client_ip.sql @@ -0,0 +1,46 @@ +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'client_ip', + '0.33.0', + 'network', + 'Returns the IP address of the client making the HTTP request. + +### Example + +```sql +insert into connection_log (client_ip) values (sqlpage.client_ip()); +``` + +### Details + +The function returns: +- The IP address of the client as a string +- `null` if the client IP cannot be determined (e.g., when serving through a Unix socket) + +### ⚠️ Important Notes for Production Use + +When [running behind a reverse proxy](/your-first-sql-website/nginx.sql) (e.g., Nginx, Apache, Cloudflare): +- This function will return the IP address of the reverse proxy, not the actual client +- To get the real client IP, use [`sqlpage.header`](?function=header): `sqlpage.header(''x-forwarded-for'')` or `sqlpage.header(''x-real-ip'')` + - The exact header name depends on your reverse proxy configuration + +Example with reverse proxy: +```sql +-- Choose the appropriate header based on your setup +select coalesce( + sqlpage.header(''x-forwarded-for''), + sqlpage.header(''x-real-ip''), + sqlpage.client_ip() +) as real_client_ip; +``` + +For security-critical applications, ensure your reverse proxy is properly configured to set and validate these headers. +' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/58_fetch_with_meta.sql b/examples/official-site/sqlpage/migrations/58_fetch_with_meta.sql new file mode 100644 index 00000000..296071d2 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/58_fetch_with_meta.sql @@ -0,0 +1,86 @@ +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'fetch_with_meta', + '0.34.0', + 'transfer-vertical', + 'Sends an HTTP request and returns detailed metadata about the response, including status code, headers, and body. + +This function is similar to [`fetch`](?function=fetch), but returns a JSON object containing detailed information about the response. +The returned object has the following structure: +```json +{ + "status": 200, + "headers": { + "content-type": "text/html", + "content-length": "1234" + }, + "body": "a string, or a json object, depending on the content type", + "error": "error message if any" +} +``` + +If the request fails or encounters an error (e.g., network issues, invalid UTF-8 response), instead of throwing an error, +the function returns a JSON object with an "error" field containing the error message. + +### Example: Basic Usage + +```sql +-- Make a request and get detailed response information +set response = sqlpage.fetch_with_meta(''https://pokeapi.co/api/v2/pokemon/ditto''); + +-- redirect the user to an error page if the request failed +select ''redirect'' as component, ''error.sql'' as url +where + json_extract($response, ''$.error'') is not null + or json_extract($response, ''$.status'') != 200; + +-- Extract data from the response json body +select ''card'' as component; +select + json_extract($response, ''$.body.name'') as title, + json_extract($response, ''$.body.abilities[0].ability.name'') as description +from $response; +``` + +### Example: Advanced Request with Authentication + +```sql +set request = json_object( + ''method'', ''POST'', + ''url'', ''https://sqlpage.free.beeceptor.com'', + ''headers'', json_object( + ''Content-Type'', ''application/json'', + ''Authorization'', ''Bearer '' || sqlpage.environment_variable(''API_TOKEN'') + ), + ''body'', json_object( + ''key'', ''value'' + ) +); +set response = sqlpage.fetch_with_meta($request); + +-- Check response content type +select ''debug'' as component, $response as response; +``` + +The function accepts the same parameters as the [`fetch` function](?function=fetch).' + ); + +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES ( + 'fetch_with_meta', + 1, + 'url', + 'Either a string containing an URL to request, or a json object in the standard format of the request interface of the web fetch API.', + 'TEXT' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/59_unsafe_contents_md.sql b/examples/official-site/sqlpage/migrations/59_unsafe_contents_md.sql new file mode 100644 index 00000000..5f8f2c97 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/59_unsafe_contents_md.sql @@ -0,0 +1,4 @@ +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'text', * FROM (VALUES +('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', TRUE, TRUE), +('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', FALSE, TRUE) +); diff --git a/examples/official-site/sqlpage/migrations/60_empty_state.sql b/examples/official-site/sqlpage/migrations/60_empty_state.sql new file mode 100644 index 00000000..4d897d4d --- /dev/null +++ b/examples/official-site/sqlpage/migrations/60_empty_state.sql @@ -0,0 +1,52 @@ +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('empty_state', 'info-circle', 'Displays a large placeholder message to communicate a single information to the user and invite them to take action. + +Typically includes a title, an optional icon/image, descriptive text (rich text formatting and images supported via Markdown), and a call-to-action button. + +Ideal for first-use screens, empty data sets, "no results" pages, or error messages.', '0.35.0'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'empty_state', * FROM (VALUES + ('title','Description of the empty state.','TEXT',TRUE,FALSE), + ('header','Text displayed on the top of the empty state.','TEXT',TRUE,TRUE), + ('icon','Name of an icon to be displayed on the top of the empty state.','ICON',TRUE,TRUE), + ('image','The URL (absolute or relative) of an image to display at the top of the empty state.','URL',TRUE,TRUE), + ('description','A short text displayed below the title.','TEXT',TRUE,TRUE), + ('link_text','The text displayed on the button.','TEXT',TRUE,FALSE), + ('link_icon','Name of an icon to be displayed on the left side of the button.','ICON',TRUE,FALSE), + ('link','The URL to which the button should navigate when clicked.','URL',TRUE,FALSE), + ('class','Class attribute added to the container in HTML. It can be used to apply custom styling to this item through css.','TEXT',TRUE,TRUE), + ('id','ID attribute added to the container in HTML. It can be used to target this item through css or for scrolling to this item through links (use "#id" in link url).','TEXT',TRUE,TRUE) +) x; + +INSERT INTO example(component, description, properties) VALUES + ('empty_state', ' +This example shows how to create a 404-style "Not Found" empty state with + - a prominent header displaying "404", + - a helpful description suggesting to adjust search parameters, and + - a "Search again" button with a search icon that links back to the search page. +', + json('[{ + "component": "empty_state", + "title": "No results found", + "header": "404", + "description": "Try adjusting your search or filter to find what you''re looking for.", + "link_text": "Search again", + "link_icon": "search", + "link": "#not-found", + "id": "not-found" + }]')), + ('empty_state', ' +It''s possible to use an icon or an image to illustrate the problem. +', + json('[{ + "component": "empty_state", + "title": "A critical problem has occurred", + "icon": "mood-wrrr", + "description_md": "SQLPage can do a lot of things, but this is not one of them. + +Please restart your browser and **cross your fingers**.", + "link_text": "Close and restart", + "link_icon": "rotate-clockwise", + "link": "#" + }]')); + diff --git a/examples/official-site/sqlpage/migrations/61_oidc_functions.sql b/examples/official-site/sqlpage/migrations/61_oidc_functions.sql new file mode 100644 index 00000000..71d1d849 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/61_oidc_functions.sql @@ -0,0 +1,294 @@ +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'user_info_token', + '0.35.0', + 'key', + '# Accessing information about the current user, when logged in with SSO + +This function can be used only when you have [configured Single Sign-On with an OIDC provider](/sso). + +## The ID Token + +When a user logs in through OIDC, your application receives an [identity token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) from the identity provider. +This token contains information about the user, such as their name and email address. +The `sqlpage.user_info_token()` function lets you access the entire contents of the ID token, as a JSON object. +You can then use [your database''s JSON functions](/blog.sql?post=JSON+in+SQL%3A+A+Comprehensive+Guide) to process that JSON. + +If you need to access a specific claim, it is easier and more performant to use the +[`sqlpage.user_info()`](?function=user_info) function instead. + +### Example: Displaying User Information + +```sql +select ''list'' as component; +select key as title, value as description +from json_each(sqlpage.user_info_token()); +``` + +This sqlite-specific example will show all the information available about the current user, such as: +- `sub`: A unique identifier for the user +- `name`: The user''s full name +- `email`: The user''s email address +- `picture`: A URL to the user''s profile picture + +### Security Notes + +- The ID token is automatically verified by SQLPage to ensure it hasn''t been tampered with. +- The token is only available to authenticated users: if no user is logged in or sso is not configured, this function returns NULL +- If some information is not available in the token, you have to configure it on your OIDC provider, SQLPage can''t do anything about it. +- The token is stored in a signed http-only cookie named `sqlpage_auth`. You can use [the cookie component](/component.sql?component=cookie) to delete it, and the user will be redirected to the login page on the next page load. +' + ); + +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'user_info', + '0.34.0', + 'user', + '# Accessing Specific User Information + +The `sqlpage.user_info` function is a convenient way to access specific pieces of information about the currently logged-in user. +When you [configure Single Sign-On](/sso), your OIDC provider will issue an [ID token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) for the user, +which contains *claims*, with information about the user. + +Calling `sqlpage.user_info(claim_name)` lets you access these claims directly from SQL. + +## How to Use + +The function takes one parameter: the name of the *claim* (the piece of information you want to retrieve). + +For example, to display a personalized welcome message, with the user''s name, you can use: + +```sql +select ''text'' as component; +select ''Welcome, '' || sqlpage.user_info(''name'') || ''!'' as title; +``` + +## Available Information + +The exact information available depends on your identity provider (the service you chose to authenticate with), +its configuration, and the scopes you requested. +Use [`sqlpage.user_info_token()`](?function=user_info_token) to see all the information available in the ID token of the current user. + +Here are some commonly available fields: + +### Basic Information +- `name`: The user''s full name (usually first and last name separated by a space) +- `email`: The user''s email address (*warning*: there is no guarantee that the user currently controls this email address. Use the `sub` claim for database references instead.) +- `picture`: URL to the user''s profile picture + +### User Identifiers +- `sub`: A unique identifier for the user (use this to uniquely identify the user in your database) +- `preferred_username`: The username the user prefers to use + +### Name Components +- `given_name`: The user''s first name +- `family_name`: The user''s last name + +## Examples + +### Personalized Welcome Message +```sql +select ''text'' as component, + ''Welcome back, **'' || sqlpage.user_info(''given_name'') || ''**!'' as contents_md; +``` + +### User Profile Card +```sql +select ''card'' as component; +select + sqlpage.user_info(''name'') as title, + sqlpage.user_info(''email'') as description, + sqlpage.user_info(''picture'') as image; +``` + +### Conditional Content Based on custom claims + +Some identity providers let you add custom claims to the ID token. +This lets you customize the behavior of your application based on arbitrary user attributes, +such as the user''s role. + +```sql +-- show everything to admins, only public items to others +select ''list'' as component; +select title from my_items + where is_public or sqlpage.user_info(''role'') = ''admin'' +``` + +## Security Best Practices + +> ⚠️ **Important**: Always use the `sub` claim to identify users in your database, not their email address. +> The `sub` claim is guaranteed to be unique and stable for each user, while email addresses can change. +> In most providers, receiving an id token with a given email does not guarantee that the user currently controls that email. + +```sql +-- Store the user''s ID in your database +insert into user_preferences (user_id, theme) +values (sqlpage.user_info(''sub''), ''dark''); +``` + +## Troubleshooting + +If you''re not getting the information you expect: + +1. Check that OIDC is properly configured in your `sqlpage.json` +2. Verify that you requested the right scopes in your OIDC configuration +3. Try using `sqlpage.user_info_token()` to see all available information +4. Check your OIDC provider''s documentation for the exact claim names they use + +Remember: If the user is not logged in or the requested information is not available, this function returns NULL. +' + ); + +INSERT INTO + sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES + ( + 'user_info', + 1, + 'claim', + 'The name of the user information to retrieve. Common values include ''name'', ''email'', ''picture'', ''sub'', ''preferred_username'', ''given_name'', and ''family_name''. The exact values available depend on your OIDC provider and configuration.', + 'TEXT' + ); + +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'oidc_logout_url', + '0.41.0', + 'logout', + '# Secure OIDC Logout + +The `sqlpage.oidc_logout_url` function generates a secure logout URL for users authenticated via [OIDC Single Sign-On](/sso). + +When a user visits this URL, SQLPage will: +1. Remove the authentication cookie +2. Redirect the user to the OIDC provider''s logout endpoint (if available) +3. Finally redirect back to the specified `redirect_uri` + +## Security Features + +This function provides protection against **Cross-Site Request Forgery (CSRF)** attacks: +- The generated URL contains a cryptographically signed token +- The token includes a timestamp and expires after 10 minutes +- The token is signed using your OIDC client secret +- Only relative URLs (starting with `/`) are allowed as redirect targets + +This means that malicious websites cannot trick your users into logging out by simply including an image or link to your logout URL. + +## How to Use + +```sql +select ''button'' as component; +select + ''Logout'' as title, + sqlpage.oidc_logout_url(''/'') as link, + ''logout'' as icon, + ''red'' as outline; +``` + +This creates a logout button that, when clicked: +1. Logs the user out of your SQLPage application +2. Logs the user out of the OIDC provider (if the provider supports [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)) +3. Redirects the user back to your homepage (`/`) + +## Examples + +### Logout Button in Navigation + +```sql +select ''shell'' as component, + ''My App'' as title, + json_array( + json_object( + ''title'', ''Logout'', + ''link'', sqlpage.oidc_logout_url(''/''), + ''icon'', ''logout'' + ) + ) as menu_item; +``` + +### Logout with Return to Current Page + +```sql +select ''button'' as component; +select + ''Sign Out'' as title, + sqlpage.oidc_logout_url(sqlpage.path()) as link; +``` + +### Conditional Logout Link + +```sql +select ''button'' as component +where sqlpage.user_info(''sub'') is not null; +select + ''Logout '' || sqlpage.user_info(''name'') as title, + sqlpage.oidc_logout_url(''/'') as link +where sqlpage.user_info(''sub'') is not null; +``` + +## Requirements + +- OIDC must be [configured](/sso) in your `sqlpage.json` +- If OIDC is not configured, this function returns NULL +- The `redirect_uri` must be a relative path starting with `/` + +## Provider Support + +The logout behavior depends on your OIDC provider: + +| Provider | Full Logout Support | +|----------|-------------------| +| Keycloak | ✅ Yes | +| Auth0 | ✅ Yes | +| Google | ❌ No (local logout only) | +| Azure AD | ✅ Yes | +| Okta | ✅ Yes | + +When the provider doesn''t support RP-Initiated Logout, SQLPage will still remove the local authentication cookie and redirect to your specified URI. +' + ); + +INSERT INTO + sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES + ( + 'oidc_logout_url', + 1, + 'redirect_uri', + 'The relative URL path where the user should be redirected after logout. Must start with `/`. Defaults to `/` if not provided.', + 'TEXT' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/62_example_cards.sql b/examples/official-site/sqlpage/migrations/62_example_cards.sql new file mode 100644 index 00000000..04a7c90d --- /dev/null +++ b/examples/official-site/sqlpage/migrations/62_example_cards.sql @@ -0,0 +1,104 @@ +create table + example_cards as +select + 'Advanced Authentication' as title, + 'user-authentication' as folder, -- Has to exactly match the folder name in the /examples/ directory + 'postgres' as db_engine, + 'Build a secure user authentication system with login, signup, and in-database session management.' as description +union all +select + 'Authenticated CRUD', + 'CRUD - Authentication', + 'sqlite', + 'Complete Create-Read-Update-Delete operations with user authentication.' +union all +select + 'Image Gallery', + 'image gallery with user uploads', + 'sqlite', + 'Create an image gallery with user uploads and session management.' +union all +select + 'Developer UI', + 'SQLPage developer user interface', + 'postgres', + 'A web-based interface for managing SQLPage files and database tables.' +union all +select + 'Corporate Game', + 'corporate-conundrum', + 'sqlite', + 'An interactive multiplayer board game with real-time updates.' +union all +select + 'Roundest Pokemon', + 'roundest_pokemon_rating', + 'sqlite', + 'Demo app with a distinct non-default design, using custom HTML templates for everything.' +union all +select + 'Todo Application', + 'todo application (PostgreSQL)', + 'postgres', + 'A full-featured todo list application with PostgreSQL backend.' +union all +select + 'MySQL & JSON', + 'mysql json handling', + 'mysql', + 'Learn advanced JSON manipulation in MySQL to build advanced SQLPage applications.' +union all +select + 'Apache Web Server', + 'web servers - apache', + 'mysql', + 'Use an existing Apache httpd Web Server to expose your SQLPage application.' +union all +select + 'Sending Emails', + 'sending emails', + 'sqlite', + 'Use the fetch function to send emails (or interact with any other HTTP API).' +union all +select + 'Simple Website', + 'simple-website-example', + 'sqlite', + 'Basic website example with navigation and data management.' +union all +select + 'Geographic App', + 'PostGIS - using sqlpage with geographic data', + 'postgres', + 'Use SQLPage to create and manage geodata.' +union all +select + 'Multi-step form', + 'forms-with-multiple-steps', + 'sqlite', + 'Guide to the implementation of forms that spread over multiple pages.' +union all +select + 'Custom HTML & JS', + 'custom form component', + 'mysql', + 'Building a custom form component with a dynamic widget using HTML and javascript.' +union all +select + 'Splitwise Clone', + 'splitwise', + 'sqlite', + 'An expense tracker app to split expenses with your friends, with nice debt charts.' +union all +select + 'Advanced Forms with MS SQL Server', + 'microsoft sql server advanced forms', + 'sql server', + 'Forms with multi-value dropdowns, using SQL Server and its JSON functions.' +union all +select + 'Rich Text Editor', + 'rich-text-editor', + 'sqlite', + 'A rich text editor with bold, italic, lists, images, and more. It posts its contents as Markdown.' +; diff --git a/examples/official-site/sqlpage/migrations/63_modal.sql b/examples/official-site/sqlpage/migrations/63_modal.sql new file mode 100644 index 00000000..dfc78c01 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/63_modal.sql @@ -0,0 +1,84 @@ +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('modal', 'app-window', ' +Defines the a temporary popup box displayed on top of a webpage’s content. +Useful for displaying additional information, help, or collect data from users. + +Modals are closed by default, and can be opened by clicking on a button or link targeting their ID.', '0.36.0'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'modal', * FROM (VALUES + ('title','Description of the modal box.','TEXT',TRUE,FALSE), + ('close','The text to display in the Close button.','TEXT',TRUE,TRUE), + ('contents','A paragraph of text to display, without any formatting, without having to make additional queries.','TEXT',FALSE,TRUE), + ('contents_md','Rich text in the markdown format. Among others, this allows you to write bold text using **bold**, italics using *italics*, and links using [text](https://example.com).','TEXT',FALSE,TRUE), + ('scrollable','Create a scrollable modal that allows scroll the modal body.','BOOLEAN',TRUE,TRUE), + ('class','Class attribute added to the container in HTML. It can be used to apply custom styling to this item through css.','TEXT',TRUE,TRUE), + ('id','ID attribute added to the container in HTML. It can be used to target this item through css or for displaying this item.','TEXT',TRUE,FALSE), + ('large','Indicates that the modal box has an increased width.','BOOLEAN',TRUE,TRUE), + ('small','Indicates that the modal box has a reduced width.','BOOLEAN',TRUE,TRUE), + ('embed','Embed remote content in an iframe.','TEXT',TRUE,TRUE), + ('embed_mode','Use "iframe" to display embedded content within an iframe.','TEXT',TRUE,TRUE), + ('height','Height of the embedded content.','INTEGER',TRUE,TRUE), + ('allow','For embedded content, this attribute specifies the features or permissions that can be used.','TEXT',TRUE,TRUE), + ('sandbox','For embedded content, this attribute specifies the security restrictions on the loaded content.','TEXT',TRUE,TRUE), + ('style','Applies CSS styles to the embedded content.','TEXT',TRUE,TRUE) +) x; + +INSERT INTO example(component, description, properties) VALUES + ('modal', + 'This example shows how to create a modal box that displays a paragraph of text. The modal window is opened with the help of a button.', + json('[ + {"component": "modal","id": "my_modal","title": "A modal box","close": "Close"}, + {"contents":"I''m a modal window, and I allow you to display additional information or help for the user."}, + {"component": "button"}, + {"title":"Open a simple modal","link":"#my_modal"} + ]') + ), + ('modal', + 'Example of modal form content', + json('[ + { + "component":"modal", + "id":"my_embed_form_modal", + "title":"Embeded form content", + "large":true, + "embed":"/examples/form.sql?_sqlpage_embed" + }, + {"component": "button"}, + {"title":"Open a modal with a form","link":"#my_embed_form_modal"} + ]') + ), + ('modal', + 'A popup modal that contains a chart generated by a separate SQL file. The modal is triggered by links inside a datagrid.', + json('[ + { + "component":"modal", + "id":"my_embed_chart_modal", + "title":"Embeded chart content", + "close":"Close", + "embed":"/examples/chart.sql?_sqlpage_embed" + }, + {"component": "datagrid"}, + {"title":"Chart", "color":"blue", "description":"Revenue", "link":"#my_embed_chart_modal"}, + {"title":"Form", "color":"green", "description":"Fill info", "link":"#my_embed_form_modal"}, + ]') + ), + ('modal', + 'Example of modal video content', + json('[ + { + "component":"modal", + "id":"my_embed_video_modal", + "title":"Embeded video content", + "close":"Close", + "embed":"https://www.youtube.com/embed/mXdgmSdaXkg", + "allow":"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", + "embed_mode":"iframe", + "height":"350" + }, + {"component": "text", "contents_md": "Open a [modal with a video](#my_embed_video_modal)"} + ]') + ); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'button', * FROM (VALUES + ('modal','Display the modal window corresponding to the specified ID.','TEXT',FALSE,TRUE) +) x; diff --git a/examples/official-site/sqlpage/migrations/64_blog_routing.sql b/examples/official-site/sqlpage/migrations/64_blog_routing.sql new file mode 100644 index 00000000..44f920b7 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/64_blog_routing.sql @@ -0,0 +1,63 @@ + +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'File-based routing in SQLPage', + 'Understanding how SQLPage maps URLs to files and handles errors', + 'route', + '2025-07-28', + ' +SQLPage uses a simple file-based routing system that maps URLs directly to SQL files in your project directory. +No complex configuration is needed. Just create files and they become accessible endpoints. + +This guide explains how SQLPage resolves URLs, handles different file types, and manages 404 errors so you can structure your application effectively. + +## How SQLPage Routes Requests + +### 1. Site Prefix Handling + +If you''ve configured a [`site_prefix`](/your-first-sql-website/nginx) in your settings, +SQLPage will redirect all requests that do not start with the prefix to `/`. + +### 2. Path Resolution Priority + +**Directory requests (paths ending with `/`)**: SQLPage looks for an `index.sql` file in that directory and executes it if found. + +**Direct SQL file requests (`.sql` extension)**: SQLPage executes the requested SQL file if it exists. + +**Static asset requests (other extensions)**: SQLPage serves files like CSS, JavaScript, images, or any other static content directly. + +**Clean URL requests (no extension)**: SQLPage first tries to find a matching `.sql` file. If that doesn''t exist but there''s an `index.sql` file in a directory with the same name, it redirects to the directory path with a trailing slash. + +### Error Handling + +When, after applying each of the rules above in order, SQLPage can''t find a requested file, +it walks up your directory structure looking for [custom `404.sql` files](/your-first-sql-website/custom_urls). + +## Dynamic Routing with SQLPage + +SQLPage''s file-based routing becomes powerful when combined with strategic use of 404.sql files to handle dynamic URLs. Here''s how to build APIs and pages with dynamic parameters: + +### Product Catalog with Dynamic IDs + +**Goal**: Handle URLs like `/products/123`, `/products/abc`, `/products/new-laptop` + +**Setup**: +```text +products/ +├── index.sql # Lists all products (/products/) +├── 404.sql # Handles /products/ +└── categories.sql # Product categories (/products/categories) +``` + +**How it works**: +- `/products/` → Executes `products/index.sql` (product listing) +- `/products/123` → No `123.sql` file exists, so executes `products/404.sql` +- `/products/laptop` → No `laptop.sql` file exists, so executes `products/404.sql` + +**In `products/404.sql`**: +```sql +set product_id = substr(sqlpage.path(), 1+length(''/products/'')); +``` + ' + ); diff --git a/examples/official-site/sqlpage/migrations/65_download.sql b/examples/official-site/sqlpage/migrations/65_download.sql new file mode 100644 index 00000000..61c005a3 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/65_download.sql @@ -0,0 +1,138 @@ +-- Insert the download component into the component table +INSERT INTO + component (name, description, icon, introduced_in_version) +VALUES + ( + 'download', + ' +The *download* component lets a page immediately return a file to the visitor. + +Instead of showing a web page, it sends the file''s bytes as the whole response, +so it should be used **at the very top of your SQL page** (before the shell or any other page contents). +It is an error to use this component after another component that would display content. + +How it works in simple terms: +- You provide the file content using a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). +A data URL is just a text string that contains both the file type and the actual data. +- Optionally, you provide a "filename" so the browser shows a proper Save As name. +If you do not provide a filename, many browsers will try to display the file inline (for example images or JSON), depending on the content type. +- You link to the page that uses the download component from another page, using the [button](/components?component=button) component for example. + +What is a data URL? +- It looks like this: `data:[content-type][;base64],DATA` +- Examples: + - Plain text (URL-encoded): `data:text/plain,Hello%20world` + - JSON (URL-encoded): `data:application/json,%7B%22message%22%3A%22Hi%22%7D` + - Binary data (Base64): `data:application/octet-stream;base64,SGVsbG8h` + +Tips: +- Use URL encoding when you have textual data. You can use [`sqlpage.url_encode(source_text)`](/functions?function=url_encode) to encode the data. +- Use Base64 when you have binary data (images, PDFs, or content that may include special characters). +- Use [`sqlpage.read_file_as_data_url(file_path)`](/functions?function=read_file_as_data_url) to read a file from the server and return it as a data URL. + +> Keep in mind that large files are better served from disk or object storage. Data URLs are best for small to medium files. +There is a big performance penalty for loading large files as data URLs, so it is not recommended. +', + 'download', + '0.37.0' + ); + +-- Insert the parameters for the download component into the parameter table +INSERT INTO + parameter ( + component, + name, + description, + type, + top_level, + optional + ) +VALUES + ( + 'download', + 'data_url', + 'The file content to send, written as a data URL (for example: data:text/plain,Hello%20world or data:application/octet-stream;base64,SGVsbG8h). The part before the comma declares the content type and whether the data is base64-encoded. The part after the comma is the actual data.', + 'TEXT', + TRUE, + FALSE + ), + ( + 'download', + 'filename', + 'The suggested name of the file to save (for example: report.csv). When set, the browser will download the file as an attachment with this name. When omitted, many browsers may try to display the file inline depending on its content type.', + 'TEXT', + TRUE, + TRUE + ); + +-- Insert usage examples of the download component into the example table +INSERT INTO + example (component, description) +VALUES + ( + 'download', + ' +## Simple plain text file +Download a small text file. The content is URL-encoded (spaces become %20). + +```sql +select + ''download'' as component, + ''data:text/plain,Hello%20SQLPage%20world!'' as data_url, + ''hello.txt'' as filename; +``` +' + ), + ( + 'download', + ' +## Download a PDF file from the server + +Download a PDF file with the proper content type so PDF readers recognize it. +Uses [`sqlpage.read_file_as_data_url(file_path)`](/functions?function=read_file_as_data_url) to read the file from the server. + +```sql +select + ''download'' as component, + ''report.pdf'' as filename, + sqlpage.read_file_as_data_url(''report.pdf'') as data_url; +``` +' + ), + ( + 'download', + ' +## Serve an image stored as a BLOB in the database + +### Automatically detect the mime type + +If you have a table with a column `content` that contains a BLOB +(depending on the database, the type may be named `BYTEA`, `BLOB`, `VARBINARY`, or `IMAGE`), +you can just return its contents directly, and SQLPage will automatically detect the mime type, +and convert it to a data URL. + +```sql +select + ''download'' as component, + content as data_url +from document +where id = $doc_id; +``` + +### Customize the mime type + +In PostgreSQL, you can use the [encode(bytes, format)](https://www.postgresql.org/docs/current/functions-binarystring.html#FUNCTION-ENCODE) function to encode the file content as Base64, +and manually create your own data URL. + +```sql +select + ''download'' as component, + ''data:'' || doc.mime_type || '';base64,'' || encode(doc.content::bytea, ''base64'') as data_url +from document as doc +where doc.id = $doc_id; +``` + + - In Microsoft SQL Server, you can use the [BASE64_ENCODE(bytes)](https://learn.microsoft.com/en-us/sql/t-sql/functions/base64-encode-transact-sql) function to encode the file content as Base64. + - In MySQL and MariaDB, you can use the [TO_BASE64(str)](https://mariadb.com/docs/server/reference/sql-functions/string-functions/to_base64) function. +' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/66_log_component.sql b/examples/official-site/sqlpage/migrations/66_log_component.sql new file mode 100644 index 00000000..65c0e7e6 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/66_log_component.sql @@ -0,0 +1,61 @@ +INSERT INTO component(name, icon, introduced_in_version, description) VALUES +('log', 'logs', '0.37.1', 'A component that writes messages to the server logs. +When a page runs, it prints your message to the terminal/console (standard error). +Use it to track what happens and troubleshoot issues. + +### Where do the messages appear? + +- Running from a terminal (Linux, macOS, or Windows PowerShell/Command Prompt): they show up in the window. +- Docker: run `docker logs `. +- Linux service (systemd): run `journalctl -u sqlpage`. +- Output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). +'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'log', * FROM (VALUES + -- top level + ('message', 'The text to write to the server logs. It is printed when the page runs.', 'TEXT', TRUE, FALSE), + ('level', 'How important the message is. One of ''trace'', ''debug'', ''info'' (default), ''warn'', ''error''. Not case-sensitive. Controls the level shown in the logs.', 'TEXT', TRUE, TRUE) +) x; + +INSERT INTO example(component, description) VALUES +('log', ' +### Record a simple message + +This writes "Hello, World!" to the server logs. + +```sql +SELECT ''log'' as component, ''Hello, World!'' as message; +``` + +Example output: + +```text +[2025-09-13T22:30:14.722Z INFO sqlpage::log from "x.sql" statement 1] Hello, World! +``` + +### Set the importance (level) + +Choose how important the message is. + +```sql +SELECT ''log'' as component, ''error'' as level, ''This is an error message'' as message; +``` + +Example output: + +```text +[2025-09-13T22:30:14.722Z ERROR sqlpage::log from "x.sql" statement 2] This is an error message +``` + +### Log dynamic information + +Include variables like a username. + +```sql +set username = ''user'' + +select ''log'' as component, + ''403 - failed for '' || coalesce($username, ''None'') as message, + ''error'' as level; +``` +') \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/67_hmac_function.sql b/examples/official-site/sqlpage/migrations/67_hmac_function.sql new file mode 100644 index 00000000..e667f03f --- /dev/null +++ b/examples/official-site/sqlpage/migrations/67_hmac_function.sql @@ -0,0 +1,137 @@ +-- HMAC function documentation and examples +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'hmac', + '0.38.0', + 'shield-lock', + 'Creates a unique "signature" for some data using a secret key. +This signature proves that the data hasn''t been tampered with and comes from someone who knows the secret. + +### What is HMAC used for? + +[**HMAC**](https://en.wikipedia.org/wiki/HMAC) (Hash-based Message Authentication Code) is commonly used to: + - **Verify webhooks**: Use HMAC to ensure only a given external service can call a given endpoint in your application. +The service signs their request with a secret key, and you verify the signature before processing the data they sent you. +Used for instance by [Stripe](https://docs.stripe.com/webhooks?verify=verify-manually), and [Shopify](https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-2-validate-the-origin-of-your-webhook-to-ensure-its-coming-from-shopify). + - **Secure API requests**: Prove that an API request comes from an authorized source + - **Generate secure tokens**: Create temporary access codes for downloads or password resets + - **Protect data**: Ensure data hasn''t been modified during transmission + +### How to use it + +The `sqlpage.hmac` function takes three inputs: +1. **Your data** - The text you want to sign (like a message or request body) +2. **Your secret key** - A password only you know (keep this safe!) +3. **Algorithm** (optional) - The hash algorithm and output format: + - `sha256` (default) - SHA-256 with hexadecimal output + - `sha256-base64` - SHA-256 with base64 output + - `sha512` - SHA-512 with hexadecimal output + - `sha512-base64` - SHA-512 with base64 output + +It returns a signature string. If someone changes even one letter in your data, the signature will be completely different. + +### Example: Verify a Webhooks signature + +When Shopify sends you a webhook (like when someone places an order), it includes a signature. Here''s how to verify it''s really from Shopify. +This supposes you store the secret key in an [environment variable](https://en.wikipedia.org/wiki/Environment_variable) named `WEBHOOK_SECRET`. + +```sql +SET body = sqlpage.request_body(); +SET secret = sqlpage.environment_variable(''WEBHOOK_SECRET''); +SET expected_signature = sqlpage.hmac($body, $secret, ''sha256''); +SET actual_signature = sqlpage.header(''X-Webhook-Signature''); + +-- redirect to an error page and stop execution if the signature does not match +SELECT + ''redirect'' as component, + ''/error.sql?err=bad_webhook_signature'' as link +WHERE $actual_signature != $expected_signature OR $actual_signature IS NULL; + +-- If we reach here, the signature is valid - process the order +INSERT INTO orders (order_data) VALUES ($body); + +SELECT ''json'' as component, ''jsonlines'' as type; +SELECT ''success'' as status; +``` + +### Example: Time-limited links + +You can create links that will be valid only for a limited time by including a signature in them. +Let''s say we have a `download.sql` page we want to link to, +but we don''t want it to be accessible to anyone who can find the link. +Sign `file_id|expires_at` with a secret. Accept only if not expired and the signature matches. + +#### Generate a signed link + +```sql +SET expires_at = datetime(''now'', ''+1 hour''); +SET token = sqlpage.hmac( + $file_id || ''|'' || $expires_at, + sqlpage.environment_variable(''DOWNLOAD_SECRET''), + ''sha256'' +); +SELECT ''/download.sql?file_id='' || $file_id || ''&expires_at='' || $expires_at || ''&token='' || $token AS link; +``` + +#### Verify the signed link + +```sql +SET expected = sqlpage.hmac( + $file_id || ''|'' || $expires_at, + sqlpage.environment_variable(''DOWNLOAD_SECRET''), + ''sha256'' +); +SELECT ''redirect'' AS component, ''/error.sql?err=expired'' AS link +WHERE $expected != $token OR $token IS NULL OR $expires_at < datetime(''now''); + +-- serve the file +``` + +### Important Security Notes + + - **Keep your secret key safe**: If your secret leaks, anyone can forge signatures and access protected pages + - **The signature is case-sensitive**: Even a single wrong letter means the signature won''t match + - **NULL handling**: Always use `IS DISTINCT FROM`, not `=` to check for hmac matches. + - `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) != $signature` will not redirect if `$signature` is NULL (the signature is absent). + - `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) IS DISTINCT FROM $signature` checks for both NULL and non-NULL values (but is not available in all SQL dialects). + - `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) != $signature OR $signature IS NULL` is the most portable solution. +' + ); + +INSERT INTO + sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES + ( + 'hmac', + 1, + 'data', + 'The input data to compute the HMAC for. Can be any text string. Cannot be NULL.', + 'TEXT' + ), + ( + 'hmac', + 2, + 'key', + 'The secret key used to compute the HMAC. Should be kept confidential. Cannot be NULL.', + 'TEXT' + ), + ( + 'hmac', + 3, + 'algorithm', + 'The hash algorithm and output format. Optional, defaults to `sha256` (hex output). Supported values: `sha256`, `sha256-base64`, `sha512`, `sha512-base64`. Defaults to `sha256`.', + 'TEXT' + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/68_login.sql b/examples/official-site/sqlpage/migrations/68_login.sql new file mode 100644 index 00000000..a545105d --- /dev/null +++ b/examples/official-site/sqlpage/migrations/68_login.sql @@ -0,0 +1,74 @@ +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('login', 'password-user', ' +The login component is an authentication form with numerous customization options. +It offers the main functionalities for this type of form. +The user can enter their username and password. +There are many optional attributes such as the use of icons on input fields, the insertion of a link to a page to reset the password, an option for the application to maintain the user''s identity via a cookie. +It is also possible to set the title of the form, display the company logo, or customize the appearance of the form submission button. + +This component should be used in conjunction with other components such as [authentication](component.sql?component=authentication) and [cookie](component.sql?component=cookie). +It does not implement any logic and simply collects the username and password to pass them to the code responsible for authentication. + +A few things to know : +- The form uses the POST method to transmit information to the destination page, +- The user''s username and password are entered into fields with the names `username` and `password`, +- To obtain the values of username and password, you must use the variables `:username` and `:password`, +- When you set the `remember_me_text` property, the variable `:remember` becomes available after form submission to check if the user checked the "remember me" checkbox. +', '0.39.0'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'login', * FROM (VALUES + ('title','Title of the authentication form.','TEXT',TRUE,TRUE), + ('enctype','Form data encoding.','TEXT',TRUE,TRUE), + ('action','An optional link to a target page that will handle the results of the form. ','TEXT',TRUE,TRUE), + ('error_message','An error message to display above the form, typically shown after a failed login attempt.','TEXT',TRUE,TRUE), + ('error_message_md','A markdown error message to display above the form, typically shown after a failed login attempt.','TEXT',TRUE,TRUE), + ('username','Label and placeholder for the user account identifier text field.','TEXT',TRUE,FALSE), + ('password','Label and placeholder for the password field.','TEXT',TRUE,FALSE), + ('username_icon','Icon to display on the left side of the input field, on the same line.','ICON',TRUE,TRUE), + ('password_icon','Icon to display on the left side of the input field, on the same line.','ICON',TRUE,TRUE), + ('image','The URL of an centered image displayed before the title.','URL',TRUE,TRUE), + ('forgot_password_text','A text for the link allowing the user to reset their password. If the text is empty, the link is not displayed.','TEXT',TRUE,TRUE), + ('forgot_password_link','The link to the page allowing the user to reset their password.','TEXT',TRUE,TRUE), + ('remember_me_text','A text for the option allowing the user to request the preservation of their identity. If the text is empty, the option is not displayed.','TEXT',TRUE,TRUE), + ('footer','A text placed at the bottom of the authentication form. If both footer and footer_md are specified, footer takes precedence.','TEXT',TRUE,TRUE), + ('footer_md','A markdown text placed at the bottom of the authentication form. Useful for creating links to other pages (creating a new account, contacting technical support, etc.).','TEXT',TRUE,TRUE), + ('validate','The text to display in the button at the bottom of the form that submits the values.','TEXT',TRUE,TRUE), + ('validate_color','The color of the button at the bottom of the form that submits the values. Omit this property to use the default color.','COLOR',TRUE,TRUE), + ('validate_shape','The shape of the validation button.','TEXT',TRUE,TRUE), + ('validate_outline','A color to outline the validation button.','COLOR',TRUE,TRUE), + ('validate_size','The size of the validation button.','TEXT',TRUE,TRUE) +) x; + +-- Insert example(s) for the component +INSERT INTO example(component, description, properties) +VALUES ( + 'login', + 'Using the main options of the login component + +When the user clicks the "Sign in" button, the form is submitted to the `/examples/show_variables.sql` page. +There, you will have access to the variables: + - `:username`: the username entered by the user + - `:password`: the password entered by the user + - `:remember`: the string "on" if the checkbox was checked, or NULL if it was not checked +', + JSON( + '[ + { + "component": "login", + "action": "/examples/show_variables", + "image": "../assets/icon.webp", + "title": "Please login to your account", + "username": "Username", + "password": "Password", + "username_icon": "user", + "password_icon": "lock", + "forgot_password_text": "Forgot your password?", + "forgot_password_link": "reset_password.sql", + "remember_me_text": "Remember me", + "footer_md": "Don''t have an account? [Register here](register.sql)", + "validate": "Sign in" + } + ]' + ) + ), + ('login', 'Most basic login form', JSON('[{"component": "login"}]')); diff --git a/examples/official-site/sqlpage/migrations/69_blog_performance_guide.sql b/examples/official-site/sqlpage/migrations/69_blog_performance_guide.sql new file mode 100644 index 00000000..6dcdf111 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/69_blog_performance_guide.sql @@ -0,0 +1,284 @@ + +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'Performance Guide', + 'Concrete advice on how to make your SQLPage webapp fast', + 'bolt', + '2025-10-31', + ' +# Performance Guide + +SQLPage is [optimized](/performance) +to allow you to create web pages that feel snappy. +This guide contains advice on how to ensure your users never wait +behind a blank screen waiting for your pages to load. + +A lot of the advice here is not specific to SQLPage, but applies +to making SQL queries fast in general. +If you are already comfortable with SQL performance optimization, feel free to jump right to +the second part of the quide: *SQLPage-specific advice*. + +## Make your queries fast + +The best way to ensure your SQLPage webapp is fast is to ensure your +database is well managed and your SQL queries are well written. +We''ll go over the most common database performance pitfalls so that you know how to avoid them. + +### Choose the right database schema + +#### Normalize (but not too much) + +Your database schema should be [normalized](https://en.wikipedia.org/wiki/Database_normalization): +one piece of information should be stored in only one place in the database. +This is a good practice that will not only make your queries faster, +but also make it impossible to store incoherent data. +You should use meaningful natural [primary keys](https://en.wikipedia.org/wiki/Primary_key) for your tables +and resort to surrogate keys (such as auto-incremented integer ids) only when the data is not naturally keyed. +Relationships between tables should be explicitly represented by [foreign keys](https://en.wikipedia.org/wiki/Foreign_key). + +```sql +-- Products table, naturally keyed by catalog_number +CREATE TABLE product ( + catalog_number VARCHAR(20) PRIMARY KEY, + name TEXT NOT NULL, + price DECIMAL(10,2) NOT NULL +); + +-- Sales table: natural key = (sale_date, store_id, transaction_number) +-- composite primary key used since no single natural attribute alone uniquely identifies a sale +CREATE TABLE sale ( + sale_date DATE NOT NULL, + store_id VARCHAR(10) NOT NULL, + transaction_number INT NOT NULL, + product_catalog_number VARCHAR(20) NOT NULL, + quantity INT NOT NULL CHECK (quantity > 0), + PRIMARY KEY (sale_date, store_id, transaction_number), + FOREIGN KEY (product_catalog_number) REFERENCES product(catalog_number), + FOREIGN KEY (store_id) REFERENCES store(store_id) +); +``` + +Always use foreign keys instead of trying to store redundant data such as store names in the sales table. + +This way, when you need to display the list of stores in your application, you don''t have to +run a slow `select distinct store from sales`, that would have to go through your millions of sales +(*even if you have an index on the store column*), you just query the tiny `stores` table directly. + +You also need to use the right [data types](https://en.wikipedia.org/wiki/Data_type) for your columns, +otherwise you will waste a lot of space and time converting data at query time. +See [postgreSQL data types](https://www.postgresql.org/docs/current/datatype.html), +[MySQL data types](https://dev.mysql.com/doc/refman/8.0/en/data-types.html), +[Microsoft SQL Server data types](https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver16), +[SQLite data types](https://www.sqlite.org/datatype3.html). + +[Denormalization](https://en.wikipedia.org/wiki/Denormalization) can be introduced +only after you have already normalized your data, and is often not required at all. + +### Use views + +Querying normalized views can be cumbersome. +`select store_name, sum(paid_eur) from sale group by store_name` +is more readable than + +```sql +select store.name, sum(sale.paid_eur) +from sales + inner join stores on sale.store_id = store.store_id +group by store_name +``` + +To work around that, you can create views that contain +useful table joins so that you do not have to duplicate them in all your queries: + +```sql +create view enriched_sales as +select sales.sales_eur, sales.client_id, store.store_name +from sales +inner join store +``` + +#### Materialized views + +Some analytical queries just have to compute aggregated statistics over large quantities of data. +For instance, you might want to compute the total sales per store, or the total sales per product. +These queries are slow to compute when there are many rows, and you might not want to run them on every request. +You can use [materialized views](https://en.wikipedia.org/wiki/Materialized_view) to cache the results of these queries. +Materialized views are views that are stored as regular tables in the database. + +Depending on the database, you might have to refresh the materialized view manually. +You can either refresh the view manually from inside your sql pages when you detect they are outdated, +or write an external script to refresh the view periodically. + +```sql +create materialized view total_sales_per_store as +select store_name, sum(sales_eur) as total_sales +from sales +group by store_name; +``` + +### Use database indices + +When a query on a large table uses non-primary column in a `WHERE`, `GROUP BY`, `ORDER BY`, or `JOIN`, +you should create an [index](https://en.wikipedia.org/wiki/Database_index) on that column. +When multiple columns are used in the query, you should create a composite index on those columns. +When creating a composite index, the order of the columns is important. +The most frequently used columns should be first. + +```sql +create index idx_sales_store_date on sale (store_id, sale_date); -- useful for queries that filter by "store" or by "store and date" +create index idx_sales_product_date on sale (product_id, sale_date); +create index idx_sales_store_product_date on sale (store_id, product_id, sale_date); +``` + +Indexes are updated automatically when the table is modified. +They slow down the insertion and deletion of rows in the table, +but speed up the retrieval of rows in queries that use the indexed columns. + +### Query performance debugging + +When a query is slow, you can use the `EXPLAIN` keyword to see how the database will execute the query. +Just add `EXPLAIN` before the query you want to analyze. + +On PostgreSQL, you can use a tool like [explain.dalibo.com](https://explain.dalibo.com/) to visualize the query plan. + +What to look for: + - Are indexes used? You should see references to the indices you created. + - Are full table scans used? Large tables should never be scanned. + - Are expensive operations used? Such as sorting, hashing, bitmap index scans, etc. + - Are operations happening in the order you expected them to? Filtering large tables should come first. + +### Vacuum your database regularly + +On PostgreSQL, you can use the [`VACUUM`](https://www.postgresql.org/docs/current/sql-vacuum.html) command to garbage-collect and analyze a database. + +On MySQL, you can use the [`OPTIMIZE TABLE`](https://dev.mysql.com/doc/refman/8.0/en/optimize-table.html) command to reorganize it on disk and make it faster. +On Microsoft SQL Server, you can use the [`DBCC DBREINDEX`](https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-dbreindex-transact-sql?view=sql-server-ver17) command to rebuild the indexes. +On SQLite, you can use the [`VACUUM`](https://www.sqlite.org/lang_vacuum.html) command to garbage-collect and analyze the database. + +### Use the right database engine + +If the amount of data you are working with is very large, does not change frequently, and you need to run complex queries on it, +you could use a specialized analytical database such as [ClickHouse](https://clickhouse.com/) or [DuckDB](https://duckdb.org/). +Such databases can be used with SQLPage by using their [ODBC](https://en.wikipedia.org/wiki/Open_Database_Connectivity) drivers. + +### Database-specific performance recommendations + + - [PostgreSQL "Performance Tips"](https://www.postgresql.org/docs/current/performance-tips.html) + - [MySQL optimization guide](https://dev.mysql.com/doc/refman/8.0/en/optimization.html) + - [Microsoft SQL Server "Monitor and Tune for Performance"](https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitor-and-tune-for-performance?view=sql-server-ver17) + - [SQLite query optimizer overview](https://www.sqlite.org/optoverview.html) + +## SQLPage-specific advice + +The best way to make your SQLPage webapp fast is to make your queries fast. +Sometimes, you just don''t have control over the database, and have to run slow queries. +This section will help you minimize the impact to your users. + +### Order matters + +SQLPage executes the queries in your `.sql` files in order. +It does not start executing a query before the previous one has returned all its results. +So, if you have to execute a slow query, put it as far down in the page as possible. + +#### No heavy computation before the shell + +Every user-facing page in a SQLPage site has a [shell](/components?component=shell). + +The first queries in any sql file (all the ones that come before the [shell](/components?component=shell)) +are executed before any data has been sent to the user''s browser. +During that time, the user will see a blank screen. +So, ensure your shell comes as early as possible, and does not require any heavy computation. +If you can make your shell entirely static (independent of the database), do so, +and it will be rendered before SQLPage even finishes acquiring a database connection. + +#### Set variables just above their first usage + +For the reasons explained above, you should avoid defining all variables at the top of your sql file. +Instead, define them just above their first usage. + +### Avoid recomputing the same data multiple times + +Often, a single page will require the same pieces of data in multiple places. +In this case, avoid recomputing it on every use inside the page. + +#### Reusing a single database record + +When that data is small, store it in a sqlpage variable as JSON and then +extract the data you need using [json operations](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). + +```sql +set product = ( + select json_object(''name'', name, ''price'', price) -- in postgres, you can simply use row_to_json(product) + from products where id = $product_id +); + +select ''alert'' as component, ''Product'' as title, $product->>''name'' as description; +``` + +#### Reusing a large query result set + +You may have a page that lets the user filter a large dataset by many different criteria, +and then displays multiple charts and tables based on the filtered data. + +In this case, store the filtered data in a temporary table and then reuse it in multiple places. + +```sql +drop table if exists filtered_products; +create temporary table filtered_products as +select * from products where + ($category is null or category = $category) and + ($manufacturer is null or manufacturer = $manufacturer); + +select ''alert'' as component, count(*) || '' products'' as title +from filtered_products; + +select ''list'' as component; +select name as title from filtered_products; +``` + +### Reduce the number of queries + +Each query you execute has an overhead of at least the time it takes to send a packet back and forth +between SQLPage and the database. +When it''s possible, combine multiple queries into a single one, possibly using +[`UNION ALL`](https://en.wikipedia.org/wiki/Set_operations_(SQL)#UNION_operator). + +```sql +select ''big_number'' as component; + +with stats as ( + select count(*) as total, avg(price) as average_price from filtered_products +) +select ''count'' as title, stats.total as value from stats +union all +select ''average price'' as title, stats.average_price as value from stats; +``` + +### Lazy loading + +Use the [card](/component?component=card) and [modal](/component?component=modal) components +with the `embed` attribute to load data lazily. +Lazy loaded content is not sent to the user''s browser when the page initially loads, +so it does not block the initial rendering of the page and provides a better experience for +data that might be slow to load. + +### Database connections + +SQLPage uses connection pooling: it keeps multiple database connections opened, +and reuses them for consecutive requests. When it does not receive requests for a long time, +it closes idle connection. When it receives many requests, it opens new connection, +but never more than the value specified by `max_database_pool_connections` in its +[configuration](https://github.com/sqlpage/SQLPage/blob/main/configuration.md). +You can increase the value of that parameter if your website has many concurrent users and your +database is configured to allow opening many simultaneous connections. + +### SQLPage performance debugging + +When `environment` is set to `development` in its [configuration](https://github.com/sqlpage/SQLPage/blob/main/configuration.md), +SQLPage will include precise measurement of the time it spends in each of the steps it has to go through before starting to send data +back to the user''s browser. You can visualize that performance data in your browser''s network inspector. + +You can set the `RUST_LOG` environment variable to `sqlpage=debug` to make SQLPage +print detailed messages associated with precise timing for everything it does. +'); diff --git a/examples/official-site/sqlpage/migrations/70_pagination.sql b/examples/official-site/sqlpage/migrations/70_pagination.sql new file mode 100644 index 00000000..5bb50d01 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/70_pagination.sql @@ -0,0 +1,252 @@ +INSERT INTO component(name, icon, description, introduced_in_version) VALUES + ('pagination', 'sailboat-2', ' +Navigation links to go to the first, previous, next, or last page of a dataset. +Useful when data is divided into pages, each containing a fixed number of rows. + +This component only handles the display of pagination. +**Your sql queries are responsible for filtering data** based on the page number passed as a URL parameter. + +This component is typically used in conjunction with a [table](?component=table), +[list](?component=list), or [card](?component=card) component. + +The pagination component displays navigation buttons (first, previous, next, last) customizable with text or icons. + +For large numbers of pages, an offset can limit the visible page links. + +A minimal example of a SQL query that uses the pagination would be: +```sql +select ''table'' as component; +select * from my_table limit 100 offset $offset; + +select ''pagination'' as component; +with recursive pages as ( + select 0 as offset + union all + select offset + 100 from pages + where offset + 100 < (select count(*) from my_table) +) +select + (offset/100+1) as contents, + sqlpage.link(sqlpage.path(), json_object(''offset'', offset)) as link, + offset = coalesce(cast($offset as integer), 0) as active +from pages; +``` + +For more advanced usage, the [pagination guide](blog.sql?post=How+to+use+the+pagination+component) provides a complete tutorial. +', '0.40.0'); + +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'pagination', * FROM (VALUES + -- Top-level parameters + ('first_link','A target URL to which the user should be directed to get to the first page. If none, the link is not displayed.','URL',TRUE,TRUE), + ('previous_link','A target URL to which the user should be directed to get to the previous page. If none, the link is not displayed.','URL',TRUE,TRUE), + ('next_link','A target URL to which the user should be directed to get to the next page. If none, the link is not displayed.','URL',TRUE,TRUE), + ('last_link','A target URL to which the user should be directed to get to the last page. If none, the link is not displayed.','URL',TRUE,TRUE), + ('first_title','The text displayed on the button to go to the first page.','TEXT',TRUE,TRUE), + ('previous_title','The text displayed on the button to go to the previous page.','TEXT',TRUE,TRUE), + ('next_title','The text displayed on the button to go to the next page.','TEXT',TRUE,TRUE), + ('last_title','The text displayed on the button to go to the last page.','TEXT',TRUE,TRUE), + ('first_disabled','disables the button to go to the first page.','BOOLEAN',TRUE,TRUE), + ('previous_disabled','disables the button to go to the previous page.','BOOLEAN',TRUE,TRUE), + ('next_disabled','Disables the button to go to the next page.','BOOLEAN',TRUE,TRUE), + ('last_disabled','disables the button to go to the last page.','BOOLEAN',TRUE,TRUE), + ('outline','Whether to use outline version of the pagination.','BOOLEAN',TRUE,TRUE), + ('circle','Whether to use circle version of the pagination.','BOOLEAN',TRUE,TRUE), + -- Item-level parameters (for each page) + ('contents','Page number.','INTEGER',FALSE,FALSE), + ('link','A target URL to which the user should be redirected to view the requested page of data.','URL',FALSE,TRUE), + ('offset','Whether to use offset to show only a few pages at a time. Usefull if the count of pages is too large. Defaults to false','BOOLEAN',FALSE,TRUE), + ('active','Whether the link is active or not. Defaults to false.','BOOLEAN',FALSE,TRUE) +) x; + + +-- Insert example(s) for the component +INSERT INTO example(component, description, properties) +VALUES ( + 'pagination', + 'This is an extremely simple example of a pagination component that displays only the page numbers, with the first page being the current page.', + JSON( + '[ + { + "component": "pagination" + }, + { + "contents": 1, + "link": "?component=pagination&page=1", + "active": true + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + } + ]' + ) + ), + ( + 'pagination', + 'The ouline style adds a rectangular border to each navigation link.', + JSON( + '[ + { + "component": "pagination", + "outline": true + }, + { + "contents": 1, + "link": "?component=pagination&page=1", + "active": true + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + } + ]' + ) + ), + ( + 'pagination', + 'The circle style adds a circular border to each navigation link.', + JSON( + '[ + { + "component": "pagination", + "circle": true + }, + { + "contents": 1, + "link": "?component=pagination&page=1", + "active": true + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + } + ]' + ) + ), + ( + 'pagination', + 'The following example implements navigation links that can be enabled or disabled as needed. Since a navigation link does not appear if no link is assigned to it, you must always assign a link to display it as disabled.', + JSON( + '[ + { + "component": "pagination", + "first_link": "?component=pagination", + "first_disabled": true, + "previous_link": "?component=pagination", + "previous_disabled": true, + "next_link": "#?page=2", + "last_link": "#?page=3" + + }, + { + "contents": 1, + "link": "?component=pagination&page=1", + "active": true + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + } + ]' + ) + ), + ( + 'pagination', + 'Instead of using icons, you can apply text to the navigation links.', + JSON( + '[ + { + "component": "pagination", + "first_title": "First", + "last_title": "Last", + "previous_title": "Previous", + "next_title": "Next", + "first_link": "?component=pagination", + "first_disabled": true, + "previous_link": "?component=pagination", + "previous_disabled": true, + "next_link": "#?page=2", + "last_link": "#?page=3" + + }, + { + "contents": 1, + "link": "?component=pagination&page=1", + "active": true + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + } + ]' + ) + ), + ( + 'pagination', + 'If you have a large number of pages to display, you can use an offset to represent a group of pages.', + JSON( + '[ + { + "component": "pagination", + "first_link": "#?page=1", + "previous_link": "#?page=3", + "next_link": "#?page=4", + "last_link": "#?page=99" + + }, + { + "contents": 1, + "link": "?component=pagination&page=1" + }, + { + "contents": 2, + "link": "?component=pagination&page=2" + }, + { + "contents": 3, + "link": "?component=pagination&page=3" + }, + { + "contents": 4, + "link": "?component=pagination&page=4", + "active": true + }, + { + "contents": 5, + "link": "?component=pagination&page=5" + }, + { + "contents": 6, + "link": "?component=pagination&page=6" + }, + { + "offset": true + }, + { + "contents": 99, + "link": "?component=pagination&page=99" + }, + ]' + ) + ); + \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/71_blog_pagination.sql b/examples/official-site/sqlpage/migrations/71_blog_pagination.sql new file mode 100644 index 00000000..859b9e8c --- /dev/null +++ b/examples/official-site/sqlpage/migrations/71_blog_pagination.sql @@ -0,0 +1,163 @@ + +INSERT INTO blog_posts (title, description, icon, created_at, content) +VALUES + ( + 'How to use the pagination component', + 'A tutorial for using the pagination component', + 'sailboat-2', + '2025-11-10', + ' +# How to use the pagination component + +To display a large number of records from a database, it is often practical to split these data into pages. The user can thus navigate from one page to another, as well as directly to the first or last page. With SQLPage, it is possible to perform these operations using the pagination component. + +This component offers many options, and I recommend consulting its documentation before proceeding with the rest of this tutorial. + +Of course, this component only handles its display and does not implement any logic for data processing or state changes. In this tutorial, we will implement a complete example of using the pagination component with a SQLite database, but the code should work without modification (or with very little modification) with any relational database management system (RDBMS). + +> This article serves as a tutorial on the pagination component, rather than an advanced guide on paginated data retrieval from a database. The document employs a straightforward approach using the LIMIT and OFFSET instructions. This approach is interesting only for datasets that are big enough not to be realistically loadable on a single webpage, yet small enough for being queryable with OFFSET...LIMIT. + +## Initialization + +We first need to define two constants that indicate the maximum number of rows per page and the maximum number of pages that the component should display. + +``` +SET MAX_RECORD_PER_PAGE = 10; +SET MAX_PAGES = 10; +``` + +Now, we need to know the number of rows present in the table to be displayed. We can then calculate the number of pages required. + +``` +SET records_count = (SELECT COUNT(*) FROM album); +SET pages_count = (CAST($records_count AS INTEGER) / CAST($MAX_RECORD_PER_PAGE AS INTEGER)); +``` + +It is possible that the number of rows in the table is greater than the estimated number of pages multiplied by the number of rows per page. In this case, it is necessary to add an additional page. + +``` +SET pages_count = ( + CASE + WHEN MOD(CAST($records_count AS INTEGER),CAST($MAX_RECORD_PER_PAGE AS INTEGER)) = 0 THEN $pages_count + ELSE (CAST($pages_count AS INTEGER) + 1) + END +); +``` + +We will need to transmit the page number to be displayed in the URL using the `page` parameter. We do the same for the number of the first page (`idx_page`) appearing at the left end of the pagination component. + +![Meaning of URL parameters](blog/pagination.png) + + +If the page number or index is not present in the URL, the value of 1 is applied by default. + +``` +SET page = COALESCE($page,1); +SET idx_page = COALESCE($idx_page,1); +``` + +## Read the data + +We can now read and display the data based on the active page. To do this, we simply use a table component. + +``` +SELECT + ''table'' as component +SELECT + user_id AS id, + last_name AS "Last name", + first_name AS "First name" +FROM + users +LIMIT CAST($MAX_RECORD_PER_PAGE AS INTEGER) +OFFSET (CAST($page AS INTEGER) - 1) * CAST($MAX_RECORD_PER_PAGE AS INTEGER); +``` + +The SQL LIMIT clause allows us to not read more rows than the maximum allowed for a page. With the SQL OFFSET clause, we specify from which row the data is selected. + +On each HTML page load, the table content will be updated based on the `page` and `idx_page` variables, whose values will be extracted from the URL + +## Set up the pagination component + +Now, we need to set up the parameters that will be included in the URL for the buttons to navigate to the previous or next page. + +If the user wants to view the previous page and the current page is not the first one, the value of the `page` variable is decremented. The same applies to `idx_page`, which is decremented if its value does not correspond to the first page. + +``` +SET previous_parameters = ( + CASE + WHEN CAST($page AS INTEGER) > 1 THEN + json_object( + ''page'', (CAST($page AS INTEGER) - 1), + ''idx_page'', (CASE + WHEN CAST($idx_page AS INTEGER) > 1 THEN (CAST($idx_page AS INTEGER) - 1) + ELSE $idx_page + END) + ) + ELSE json_object() END +); +``` + +The logic is quite similar for the URL to view the next page. First, it is necessary to verify that the user is not already on the last page. Then, the `page` variable can be incremented and the `idx_page` variable updated. + +``` +SET next_parameters = ( + CASE + WHEN CAST($page AS INTEGER) < CAST($pages_count AS INTEGER) THEN + json_object( + ''page'', (CAST($page AS INTEGER) + 1), + ''idx_page'', (CASE + WHEN CAST($idx_page AS INTEGER) < (CAST($pages_count AS INTEGER) - CAST($MAX_PAGES AS INTEGER) + 1) THEN (CAST($idx_page AS INTEGER) + 1) + ELSE $idx_page + END) + ) + ELSE json_object() END +); +``` + +We can now add the pagination component, which is placed below the table displaying the data. All the logic for managing the buttons is entirely handled in SQL: +- the buttons to access the first or last page, +- the buttons to view the previous or next page, +- the enabling or disabling of these buttons based on the context. + +``` +SELECT + ''pagination'' AS component, + (CAST($page AS INTEGER) = 1) AS first_disabled, + (CAST($page AS INTEGER) = 1) AS previous_disabled, + (CAST($page AS INTEGER) = CAST($pages_count AS INTEGER)) AS next_disabled, + (CAST($page AS INTEGER) = CAST($pages_count AS INTEGER)) AS last_disabled, + sqlpage.link(sqlpage.path(), json_object(''page'', 1, ''idx_page'', 1)) as first_link, + sqlpage.link(sqlpage.path(), $previous_parameters) AS previous_link, + sqlpage.link(sqlpage.path(), $next_parameters) AS next_link, + sqlpage.link( + sqlpage.path(), + json_object(''page'', $pages_count, ''idx_page'', ( + CASE + WHEN (CAST($pages_count AS INTEGER) <= CAST($MAX_PAGES AS INTEGER)) THEN 1 + ELSE (CAST($pages_count AS INTEGER) - CAST($MAX_PAGES AS INTEGER) + 1) + END) + ) + ) AS last_link, + TRUE AS outline; +``` + +The final step is to generate the page numbers based on the number of pages and the index of the first page displayed to the left of the component. To do this, we use a recursive CTE query. + +``` +WITH RECURSIVE page_numbers AS ( + SELECT $idx_page AS number + UNION ALL + SELECT number + 1 + FROM page_numbers + LIMIT CAST($MAX_PAGES AS INTEGER) +) +SELECT + number AS contents, + sqlpage.link(sqlpage.path(), json_object(''page'', number, ''idx_page'', $idx_page)) as link, + (number = CAST($page AS INTEGER)) AS active +FROM page_numbers; +``` + +If the added page matches the content of the `page` variable, the `active` option is set to `TRUE` so that the user knows it is the current page. +'); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/72_set_variable.sql b/examples/official-site/sqlpage/migrations/72_set_variable.sql new file mode 100644 index 00000000..1a213656 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/72_set_variable.sql @@ -0,0 +1,60 @@ +INSERT INTO + sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES + ( + 'set_variable', + '0.40.0', + 'variable', + 'Returns a URL that is the same as the current page''s URL, but with a variable set to a new value. + +This function is useful when you want to create a link that changes a parameter on the current page, while preserving other parameters. + +It is equivalent to `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables(''get''), json_object(name, value)))`. + +### Example + +Let''s say you have a list of products, and you want to filter them by category. You can use `sqlpage.set_variable` to create links that change the category filter, without losing other potential filters (like a search query or a sort order). + +```sql +select ''button'' as component, ''sm'' as size, ''center'' as justify; +select + category as title, + sqlpage.set_variable(''category'', category) as link, + case when $category = category then ''primary'' else ''secondary'' end as color +from categories; +``` + +### Parameters + - `name` (TEXT): The name of the variable to set. + - `value` (TEXT): The value to set the variable to. If `NULL` is passed, the variable is removed from the URL. +' + ); + +INSERT INTO + sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES + ( + 'set_variable', + 1, + 'name', + 'The name of the variable to set.', + 'TEXT' + ), + ( + 'set_variable', + 2, + 'value', + 'The value to set the variable to.', + 'TEXT' + ); diff --git a/examples/official-site/sqlpage/migrations/999_fts_search_index.sql b/examples/official-site/sqlpage/migrations/999_fts_search_index.sql new file mode 100644 index 00000000..225ff429 --- /dev/null +++ b/examples/official-site/sqlpage/migrations/999_fts_search_index.sql @@ -0,0 +1,32 @@ +CREATE VIRTUAL TABLE documentation_fts USING fts5( + component_name, + component_description, + parameter_name, + parameter_description, + blog_title, + blog_description, + function_name, + function_description, + function_parameter_name, + function_parameter_description, + component_example_description, + component_example_json +); + +INSERT INTO documentation_fts(component_name, component_description) +SELECT name, description FROM component; + +INSERT INTO documentation_fts(component_name, parameter_name, parameter_description) +SELECT component, name, description FROM parameter; + +INSERT INTO documentation_fts(blog_title, blog_description) +SELECT title, description FROM blog_posts; + +INSERT INTO documentation_fts(function_name, function_description) +SELECT name, description_md FROM sqlpage_functions; + +INSERT INTO documentation_fts(function_name, function_parameter_name, function_parameter_description) +SELECT function, name, description_md FROM sqlpage_function_parameters; + +INSERT INTO documentation_fts(component_name, component_example_description, component_example_json) +SELECT component, description, properties FROM example; \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql index 47e66576..a5d438c8 100644 --- a/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql +++ b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql @@ -18,7 +18,9 @@ FROM (VALUES ('title', TRUE), ('tracking', TRUE), ('text', TRUE), - ('carousel', TRUE) + ('carousel', TRUE), + ('login', TRUE), + ('pagination', TRUE) ); INSERT INTO parameter(component, top_level, name, description, type, optional) @@ -49,6 +51,8 @@ FROM (VALUES ('timeline', FALSE), ('title', TRUE), ('tracking', TRUE), - ('carousel', TRUE) + ('carousel', TRUE), + ('login', TRUE), + ('pagination', TRUE) ); diff --git a/examples/official-site/sqlpage/sqlpage.yaml b/examples/official-site/sqlpage/sqlpage.yaml index d159198d..b9c67cd9 100644 --- a/examples/official-site/sqlpage/sqlpage.yaml +++ b/examples/official-site/sqlpage/sqlpage.yaml @@ -1,5 +1,7 @@ # The documentation site is fully static, so we don't need to persist any data. -database_url: "sqlite::memory:" +database_url: "sqlite::memory:?cache=shared" # We have a file upload example, and would like to limit the size of the uploaded files -max_uploaded_file_size: 256000 \ No newline at end of file +max_uploaded_file_size: 256000 + +database_connection_acquire_timeout_seconds: 30 \ No newline at end of file diff --git a/examples/official-site/sqlpage/templates/color_swatch.handlebars b/examples/official-site/sqlpage/templates/color_swatch.handlebars new file mode 100644 index 00000000..e27fe787 --- /dev/null +++ b/examples/official-site/sqlpage/templates/color_swatch.handlebars @@ -0,0 +1,20 @@ +
+ {{#each_row}} +
+
+
+
+

{{name}}

+ {{#if hex}} +
{{hex}}
+ {{/if}} + {{#if css_var}} +
{{css_var}}
+ {{/if}} + {{#if description}} +

{{description}}

+ {{/if}} +
+
+ {{/each_row}} +
diff --git a/examples/official-site/sqlpage/templates/shell-home.handlebars b/examples/official-site/sqlpage/templates/shell-home.handlebars new file mode 100644 index 00000000..b4203fa4 --- /dev/null +++ b/examples/official-site/sqlpage/templates/shell-home.handlebars @@ -0,0 +1,1543 @@ + + + + + + + SQLPage - SQL websites + + + + + + + + + + + + + + +
+ + +
+

SQLPage + Website + App + Tools + Forms + Maps + Plots + APIs +

+

Instant web interfaces for your database. Free and open-source.

+ Try Online +
+
+ + + + + + +
+
+
+

Build sophisticated tools, easily

+

Turn simple SQL queries into beautiful, dynamic web applications.

+
+
+
+
📈
+

Complete

+

Create navigatable interfaces with + maps, + charts, + tables, + forms, + grids, + dashboards, and more. + Batteries included. +

+
+
+
+

Fast

+

Build fast applications, quickly. You spend one afternoon building your first app. Then it + loads instantly for everyone forever. +

+
+
+
🎯
+

Easy

+

You can teach yourself enough SQL to query and edit a database through SQLPage in a + weekend. Focus on your data, we'll handle optimizations and security.

+
+
+
+
+ +
+
+
+

More scalable than a spreadsheet

+

SQL queries sort, filter, and aggregate millions of rows in milliseconds. No more slow spreadsheet + formulas or memory limitations. Your app remains smooth and responsive + even + as your data grows.

+
+
+ +
+
+
+ +
+
+
+

More dynamic than a dashboard

+

Create multi-page interactive websites with drill-down capabilities. Navigate from summaries to + detailed records. +

+ You should be able to comment, edit, and dive into your data instead of just looking at aggregated + statistics. +

+ Build apps, not dashboards. +

+
+ User creation form, illustrating the ability to create, edit, and delete individual data points, to go beyond simple static dashboards. +
+
+
+ +
+
+
+
Database Compatibility
+

Works with your database

+

SQLPage connects to the database engine you already rely on today. + Keep your data in place and surface it through a + friendly interface that stays in sync. + If you don't have a DB yet, SQLPage comes with a built-in query engine. +

+
+
SQLite built-in
+
MySQL & MariaDB
+
PostgreSQL family
+
Microsoft SQL Server
+
ODBC bridge
+
+
+
+
+
+ Native connectors +
+
SQLite
+
MySQL
+
PostgreSQL
+
Microsoft SQL Server
+
+
+

Wherever your data lives

+

Through ODBC you can plug SQLPage into any warehouses and enterprise engines.

+
+ + + + + + + + +
+
+ No data copy + Streams query results +
+
+
+
+
+ +
+
+
+

The power of the web, without the complexity

+

Do not code, query. Write SQL, get a web app. +

Traditional web programming languages are powerful, but complex. + Using smart opinionated defaults, SQLPage requires 10x less code. +

+ For advanced customization, you can still optionally use HTML/CSS/JS, and + integrate with external programs and APIs.

+
+ Simple Web Development. Just SQL +
+
+ + + + + \ No newline at end of file diff --git a/examples/official-site/sqlpage/templates/typography_sample.handlebars b/examples/official-site/sqlpage/templates/typography_sample.handlebars new file mode 100644 index 00000000..647bcb0d --- /dev/null +++ b/examples/official-site/sqlpage/templates/typography_sample.handlebars @@ -0,0 +1,18 @@ +
+{{#each_row}} +
+ {{#if title}} +
{{title}}
+ {{/if}} +
+ {{sample_text}} +
+ {{#if description}} +
{{description}}
+ {{/if}} + {{#if usage}} +
Use for: {{usage}}
+ {{/if}} +
+{{/each_row}} +
diff --git a/examples/official-site/sqlpage_cover_image.webp b/examples/official-site/sqlpage_cover_image.webp new file mode 100644 index 00000000..5efac872 Binary files /dev/null and b/examples/official-site/sqlpage_cover_image.webp differ diff --git a/examples/official-site/sqlpage_illustration_alien.webp b/examples/official-site/sqlpage_illustration_alien.webp new file mode 100644 index 00000000..7f264580 Binary files /dev/null and b/examples/official-site/sqlpage_illustration_alien.webp differ diff --git a/examples/official-site/sqlpage_illustration_components.webp b/examples/official-site/sqlpage_illustration_components.webp new file mode 100644 index 00000000..49886236 Binary files /dev/null and b/examples/official-site/sqlpage_illustration_components.webp differ diff --git a/examples/official-site/sqlpage_social_preview.webp b/examples/official-site/sqlpage_social_preview.webp new file mode 100644 index 00000000..3b46c9d3 Binary files /dev/null and b/examples/official-site/sqlpage_social_preview.webp differ diff --git a/examples/official-site/sso/index.sql b/examples/official-site/sso/index.sql new file mode 100644 index 00000000..c352d9e6 --- /dev/null +++ b/examples/official-site/sso/index.sql @@ -0,0 +1,7 @@ +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; + +select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; + +select 'text' as component, sqlpage.read_file_as_text('sso/single_sign_on.md') as contents_md, true as article; \ No newline at end of file diff --git a/examples/official-site/sso/single_sign_on.md b/examples/official-site/sso/single_sign_on.md new file mode 100644 index 00000000..d10e6b67 --- /dev/null +++ b/examples/official-site/sso/single_sign_on.md @@ -0,0 +1,161 @@ +# Setting Up Single Sign-On in SQLPage + +When you want to add user authentication to your SQLPage application, you have two main options: + +1. The [authentication component](/component.sql?component=authentication): + A simple username/password system, that you have to manage yourself. +2. **OpenID Connect (OIDC)**: + A single sign-on system that lets users log in with their existing accounts (like Google, Microsoft, or your organization's own identity provider). + +This guide will help you set up single sign-on using OpenID connect with SQLPage quickly. + +## Essential Terms + +- **OIDC** ([OpenID Connect](https://openid.net/developers/how-connect-works/)): The protocol that enables secure login with existing accounts. While it adds some complexity, it's an industry standard that ensures your users' data stays safe. +- **Issuer** (or identity provider): The service that verifies your users' identity (like Google or Microsoft) +- **Identity Token**: A secure message from the issuer containing user information. It is stored as a cookie on the user's computer, and sent with every request after login. SQLPage will redirect all requests that do not contain a valid token to the identity provider's login page. +- **Claim**: A piece of information contained in the token about the user (like their name or email) + +## Quick Setup Guide + +### Choose an OIDC Provider + +Here are the setup guides for +[Google](https://developers.google.com/identity/openid-connect/openid-connect), +[Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app), +and [Keycloak](https://www.keycloak.org/getting-started/getting-started-docker) (self-hosted). + +### Register Your Application + +1. Go to your chosen provider's developer console +2. Create a new application +3. Set the redirect URI to `http://localhost:8080/sqlpage/oidc_callback`. (We will change that later when you deploy your site to a hosting provider such as [datapage](https://beta.datapage.app/)). +4. Note down the client ID and client secret + +### Configure SQLPage + +Create or edit `sqlpage/sqlpage.json` to add the following configuration keys: + +```json +{ + "oidc_issuer_url": "https://accounts.google.com", + "oidc_client_id": "your-client-id", + "oidc_client_secret": "your-client-secret", + "host": "localhost:8080" +} +``` + +#### Provider-specific settings +- Google: `https://accounts.google.com` +- Microsoft: `https://login.microsoftonline.com/{tenant}/v2.0`. [Find your value of `{tenant}`](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-create-new-tenant). +- GitHub: `https://github.com` +- Keycloak: Use [your realm's base url](https://www.keycloak.org/securing-apps/oidc-layers), ending in `/auth/realms/{realm}`. +- For other OIDC providers, you can usually find the issuer URL by + looking for a "discovery document" or "well-known configuration" at an URL that ends with the suffix `/.well-known/openid-configuration`. + Strip the suffix and use it as the `oidc_issuer_url`. + +### Restart SQLPage + +When you restart your SQLPage instance, it should automatically contact +the identity provider, find its login URL, and the public keys that will be used to check the validity of its identity tokens. + +By default, all pages on your website will now require users to log in. + +## Access User Information in Your SQL + +Once you have successfully configured SSO, you can access information +about the authenticated user who is visiting the current page using the following functions: +- [`sqlpage.user_info`](/functions.sql?function=user_info) to access a particular claim about the user such as `name` or `email`, +- [`sqlpage.user_info_token`](/functions.sql?function=user_info_token) to access the entire identity token as json. + +Access user data in your SQL files: + +```sql +select 'text' as component, ' + +Welcome, ' || sqlpage.user_info('name') || '! + +You have visited this site ' || + (select count(*) from page_visits where user=sqlpage.user_info('sub')) || +' times before. +' as contents_md; + +insert into page_visits + (path, user) +values + (sqlpage.path(), sqlpage.user_info('sub')); +``` + +## Restricting authentication to a specific set of pages + +Sometimes, you don't want to protect your entire website with a login, but only a specific section. +You can achieve this by adding the `oidc_protected_paths` option to your `sqlpage.json` file. + +This option takes a list of URL prefixes. If a user requests a page whose address starts with one of these prefixes, they will be required to log in. + +**Example:** Protect only pages in the `/admin` folder. + +```json +{ + "oidc_issuer_url": "https://accounts.google.com", + "oidc_client_id": "your-client-id", + "oidc_client_secret": "your-client-secret", + "host": "localhost:8080", + "oidc_protected_paths": ["/admin"] +} +``` + +In this example, a user visiting `/admin/dashboard.sql` will be prompted to log in, while a user visiting `/index.sql` will not. + +### Creating a public login page + +A common pattern is to have a public home page with a "Login" button that redirects users to a protected area. + +With the configuration above, you can create a public page `login.sql` that is not in a protected path. This page can contain a simple link to a protected resource, for instance `/admin/index.sql`: + +```sql +select 'list' as component, 'actions' as title; +select 'Login' as title, '/admin' as link, 'login' as icon; +``` + +When a non-authenticated user clicks this "Login" link, SQLPage will automatically redirect them to your identity provider's login page. After they successfully authenticate, they will be sent back to the page they originally requested (`/admin/index.sql`). + +## Going to Production + +When deploying to production: + +1. Update the redirect URI in your OIDC provider's settings to: + ``` + https://your-domain.com/sqlpage/oidc_callback + ``` + +2. Update your `sqlpage.json`: + ```json + { + "oidc_issuer_url": "https://accounts.google.com", + "oidc_client_id": "your-client-id", + "oidc_client_secret": "your-client-secret", + "host": "your-domain.com" + } + ``` + +3. If you're using HTTPS (recommended), make sure your `host` setting matches your domain name exactly. + +## Troubleshooting + +### Version Requirements +- OIDC support requires SQLPage **version 0.35 or higher**. Check your version in the logs. + +### Common Configuration Issues +- **Redirect URI Mismatch**: The redirect URI in your OIDC provider settings must exactly match `https://your-domain.com/sqlpage/oidc_callback` (or `http://localhost:8080/sqlpage/oidc_callback` for local development) +- **Invalid Client Credentials**: Double-check your client ID and secret are copied correctly from your OIDC provider +- **Host Configuration**: The `host` setting in `sqlpage.json` must match your application's domain name exactly +- **HTTPS Requirements**: Most OIDC providers require HTTPS in production. Ensure your site is served over HTTPS. +- **Provider Discovery**: If SQLPage fails to discover your provider's configuration, verify the `oidc_issuer_url` is correct and accessible by loading `{oidc_issuer_url}/.well-known/openid-configuration` in your browser. + +### Debugging Tips +- Check SQLPage's logs for detailed error messages. You can enable verbose logging with the `RUST_LOG=trace` environment variable. +- Verify your OIDC provider's logs for authentication attempts +- In production, confirm your domain name matches exactly in both the OIDC provider settings and `sqlpage.json` +- If [using a reverse proxy](/your-first-sql-website/nginx.sql), ensure it's properly configured to handle the OIDC callback path. +- If you have checked everything and you think the bug comes from SQLPage itself, [open an issue on our bug tracker](https://github.com/sqlpage/SQLPage/issues). \ No newline at end of file diff --git a/examples/official-site/style_pricing.css b/examples/official-site/style_pricing.css new file mode 100644 index 00000000..9bb0b1c0 --- /dev/null +++ b/examples/official-site/style_pricing.css @@ -0,0 +1,17 @@ +/* Ensure all unordered and ordered lists are left-aligned */ +ul, +ol { + text-align: left; /* Aligns bullet points to the left */ + margin-left: 20px; /* Adds indentation for better readability */ + padding-left: 20px; /* Adds space between bullet and text */ +} + +/* Optional: Style for the individual list items */ +li { + margin-bottom: 10px; /* Adds space between list items */ +} + +/* Optional: Ensure the body text is also left-aligned */ +body { + text-align: left; /* Makes sure the overall page is left-aligned */ +} diff --git a/examples/official-site/visual-identity.sql b/examples/official-site/visual-identity.sql new file mode 100644 index 00000000..5427d72c --- /dev/null +++ b/examples/official-site/visual-identity.sql @@ -0,0 +1,266 @@ +select 'http_header' as component, + 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; + +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'Visual Identity - SQLPage', + 'css', '/assets/highlightjs-and-tabler-theme.css', + 'theme', 'dark' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +select 'text' as component, 'Visual Identity Guide' as title, ' +This guide defines the visual identity of SQLPage for consistent brand representation. +' as contents_md; + +select 'text' as component, 'Personality' as title, ' +**Playful yet professional**: Approachable, innovative, confident, energetic, reliable, creative. +' as contents_md; + +select 'text' as component, 'Logo' as title, ' +Primary logo: `/assets/icon.webp` + +**Usage**: +- Minimum size: 48px height +- Clear space: 50% of logo height +- Do not distort, rotate, or modify +- Works on dark and light backgrounds +' as contents_md; + +select 'html' as component, ' +
+ SQLPage Logo +
+' as html; + +select 'button' as component; +select + 'Download Logo' as title, + '/assets/icon.webp' as link, + 'icon.webp' as download, + 'blue' as color, + 'download' as icon; + +select 'text' as component, 'Colors' as title, ' +Color palette extracted directly from the logo and design system. +' as contents_md; + +select 'color_swatch' as component; +select + 'Primary Cyan' as name, + '#37E5EF' as hex, + 'Main logo color - bright cyan' as description; +select + 'Teal Accent' as name, + '#2A9FAF' as hex, + 'Secondary teal from logo' as description; +select + 'Dark Navy' as name, + '#090D19' as hex, + 'Logo background - dark navy' as description; +select + 'Medium Blue' as name, + '#27314C' as hex, + 'Medium blue from logo' as description; +select + 'Blue Gray' as name, + '#304960' as hex, + 'Blue-gray from logo' as description; +select + 'Neutral Gray' as name, + '#4B4E5C' as hex, + 'Neutral gray from logo' as description; +select + 'Light Gray' as name, + '#9FA4AE' as hex, + 'Light gray from logo' as description; +select + 'Primary Background' as name, + '#0a0f1a' as hex, + 'Dark theme foundation' as description; +select + 'Primary Text' as name, + '#f7f7f7' as hex, + 'Main text color' as description; +select + 'White' as name, + '#ffffff' as hex, + 'Headings and emphasis' as description; + +select 'text' as component, 'Gradient' as title, ' +Primary gradient flows from primary cyan (#37E5EF) to teal accent (#2A9FAF). + +Use for buttons, highlights, and important elements. +' as contents_md; + +select 'text' as component, 'Typography' as title, ' +**Primary Font**: Inter + +Use Inter for all digital materials, websites, and presentations. Inter is a modern, highly legible sans-serif typeface designed specifically for user interfaces. + +**Font Source**: [Google Fonts - Inter](https://fonts.google.com/specimen/Inter) + +**Fallback Font Stack**: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif + +If Inter is not available, use the fallback stack in order. +' as contents_md; + +select 'typography_sample' as component; +select + 'Page Title' as title, + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' as font_family, + '64px' as font_size, + '800' as font_weight, + '1.1' as line_height, + '#ffffff' as text_color, + '-1px' as letter_spacing, + 'SQLPage Visual Identity' as sample_text, + 'Hero sections, main page titles, presentation title slides' as usage, + 'Bold, impactful text for maximum visual hierarchy' as description; + +select 'typography_sample' as component; +select + 'Section Heading' as title, + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' as font_family, + '56px' as font_size, + '700' as font_weight, + '1.2' as line_height, + '#ffffff' as text_color, + 'normal' as letter_spacing, + 'Section Title' as sample_text, + 'Major section breaks, chapter headings, presentation section slides' as usage, + 'Strong but slightly less prominent than page titles' as description; + +select 'typography_sample' as component; +select + 'Subsection Heading' as title, + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' as font_family, + '40px' as font_size, + '600' as font_weight, + '1.3' as line_height, + '#ffffff' as text_color, + 'normal' as letter_spacing, + 'Subsection Heading' as sample_text, + 'Card titles, subsection headers, content slide titles' as usage, + 'Clear hierarchy for organizing content' as description; + +select 'typography_sample' as component; +select + 'Body Text' as title, + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' as font_family, + '16px' as font_size, + '400' as font_weight, + '1.6' as line_height, + '#f7f7f7' as text_color, + 'normal' as letter_spacing, + 'This is body text used for paragraphs, descriptions, and main content. It should be comfortable to read with adequate spacing between lines.' as sample_text, + 'Paragraphs, descriptions, main content, presentation body text' as usage, + 'Standard reading size with comfortable line spacing' as description; + +select 'typography_sample' as component; +select + 'Small Text' as title, + 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' as font_family, + '14px' as font_size, + '400' as font_weight, + '1.5' as line_height, + '#999999' as text_color, + 'normal' as letter_spacing, + 'Small text for captions and secondary information' as sample_text, + 'Captions, metadata, footnotes, fine print' as usage, + 'Supporting information that should not compete with main content' as description; + +select 'text' as component, 'Spacing' as title, ' +**Base unit**: 8 pixels + +**Spacing scale**: +- Extra small: 8 pixels +- Small: 16 pixels +- Medium: 24 pixels +- Large: 32 pixels +- Extra large: 48 pixels +- XXL: 64 pixels + +**Container**: Maximum width 1000 pixels, padding 40 pixels +' as contents_md; + +select 'text' as component, 'Dark Environments' as title, ' +For digital displays, presentations, and screens. +' as contents_md; + +select 'text' as component, 'Background Colors' as title, ' +- **Primary background**: #0a0f1a (deep navy blue) +- **Secondary background**: #0f1426 (slightly lighter navy) +- Use gradients with primary cyan (#37E5EF) and teal (#2A9FAF) for visual interest +' as contents_md; + +select 'text' as component, 'Text Colors' as title, ' +- **Primary text**: #f7f7f7 (almost white) - for main content +- **Secondary text**: #999999 (medium gray) - for supporting information +- **Headings**: #ffffff (pure white) - for maximum emphasis +- **Links**: #7db3e8 (bright blue) - for interactive elements +' as contents_md; + +select 'text' as component, 'Contrast Guidelines' as title, ' +- All text must meet WCAG AA contrast requirements (minimum 4.5:1 for normal text, 3:1 for large text) +- Primary text (#f7f7f7) on primary background (#0a0f1a) meets accessibility standards +- Use white (#ffffff) only for headings and emphasis +- Test all color combinations before finalizing designs +' as contents_md; + +select 'text' as component, 'Light Environments' as title, ' +For print materials, light-themed websites, and bright displays. +' as contents_md; + +select 'text' as component, 'Background Colors' as title, ' +- **Primary background**: #ffffff (white) or #f8f9fa (off-white) +- **Secondary background**: #f1f3f5 (light gray) +- Use subtle gradients or solid light colors +- Avoid pure white backgrounds in print to reduce glare +' as contents_md; + +select 'text' as component, 'Text Colors' as title, ' +- **Primary text**: #1a1a1a (near black) or #212529 (dark gray) - for main content +- **Secondary text**: #6c757d (medium gray) - for supporting information +- **Headings**: #000000 (black) or #0a0f1a (dark navy) - for emphasis +- **Links**: #2A9FAF (teal) or #37E5EF (cyan) - maintain brand colors +' as contents_md; + +select 'text' as component, 'Logo Usage in Light Environments' as title, ' +- Logo works on both light and dark backgrounds +- On light backgrounds, ensure sufficient contrast +- Consider using a darker version or adding a subtle shadow if needed +- Test logo visibility on various light backgrounds +' as contents_md; + +select 'text' as component, 'Print Guidelines' as title, ' +- Use CMYK color mode for print materials +- Convert hex colors to CMYK equivalents +- Primary cyan (#37E5EF) prints as: C: 76%, M: 0%, Y: 0%, K: 6% +- Teal accent (#2A9FAF) prints as: C: 76%, M: 9%, Y: 0%, K: 31% +- Test print samples to ensure color accuracy +- Use off-white paper (#f8f9fa equivalent) to reduce eye strain +- Minimum font size for print: 10 points (13 pixels) +- Ensure all text meets print contrast requirements +' as contents_md; + +select 'text' as component, 'Presentations' as title, ' +**Background**: Dark theme #0a0f1a with gradient overlays + +**Typography**: +- Title slide: Large bold text with gradient effect +- Body: Minimum readable size for your audience +- Code: Monospace font, minimum readable size + +**Logo**: +- Title slide: Large, centered +- Content slides: Small, bottom-right corner + +**Colors**: Use brand cyan/teal gradients (#37E5EF to #2A9FAF) for highlights. Maintain high contrast for readability. +' as contents_md; + +select 'text' as component, 'Resources' as title, ' +- Logo: `/assets/icon.webp` +- CSS Theme: `/assets/highlightjs-and-tabler-theme.css` +- [Components Documentation](/component.sql) +- [GitHub Discussions](https://github.com/sqlpage/SQLPage/discussions) +' as contents_md; diff --git a/examples/official-site/your-first-sql-website/custom_urls.sql b/examples/official-site/your-first-sql-website/custom_urls.sql new file mode 100644 index 00000000..53e2dab7 --- /dev/null +++ b/examples/official-site/your-first-sql-website/custom_urls.sql @@ -0,0 +1,56 @@ +select 'http_header' as component, + 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; + +select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; + +select 'hero' as component, + 'Custom URLs' as title, + 'SQLPage lets you customize responses to URLs that don''t match any file, using `404.sql`.' as description_md, + 'not_found.jpg' as image; + +select 'text' as component, ' +# Handling custom URLs + +By default, SQLPage serves the file that matches the URL requested by the client. +If your users enter `https://example.com/about`, SQLPage will serve the file `about/index.sql` in your project. +If you create a file named `about.sql`, SQLPage will serve it when the user requests either `https://example.com/about.sql` or `https://example.com/about` (since v0.33, the `.sql` suffix is optional). + +But what if you want to handle URLs that don''t match any file in your project ? +For example, what if you have a blog, and you want nice urls like `example.com/blog/my-trip-to-rome`, +but you don''t want to create a file for each blog post ? +By default, SQLPage would return a sad 404 error if you don''t have a file named `blog/my-trip-to-rome/index.sql` +in your project''s root directory. + +But you can customize this behavior by creating a file named `404.sql` in your project. + +## The 404.sql file + +When SQLPage doesn''t find a file that matches the URL requested by the client, it will serve the file `404.sql` if it exists. + +Since v0.28, when SQLPage receives a request for a URL like `https://example.com/a/b/c`, it will look for the file `a/b/c/index.sql` in your project, +and if it doesn''t find it, it will then search for, in order: +- `/a/b/404.sql` +- `/a/404.sql` +- `/404.sql` + +## Basic routing example + +So, you have a `blog_posts` table in your database, with columns `name`, and `content`. +You want to serve the content of the blog post with id `:id` when the user requests `example.com/blog/:id`. +You can do this by creating a `404.sql` file in the `blog` directory of your project: + +```sql +-- blog/404.sql + +-- Get the id from the URL +set name = substr(sqlpage.path(), 1+length(''/blog/'')); + +-- Get the blog post from the database +select ''text'' as component, + content as contents_md +from blog_posts +where name = $name; +``` + +Now, when a user requests `example.com/blog/my-trip-to-rome`, SQLPage will serve the content of the blog post with name `my-trip-to-rome` from the `blog_posts` table. +' as contents_md; \ No newline at end of file diff --git a/examples/official-site/your-first-sql-website/first-sql-website-launch.png b/examples/official-site/your-first-sql-website/first-sql-website-launch.png index acf93e5e..1727c596 100644 Binary files a/examples/official-site/your-first-sql-website/first-sql-website-launch.png and b/examples/official-site/your-first-sql-website/first-sql-website-launch.png differ diff --git a/examples/official-site/your-first-sql-website/get_started.webp b/examples/official-site/your-first-sql-website/get_started.webp new file mode 100644 index 00000000..9ee1dce3 Binary files /dev/null and b/examples/official-site/your-first-sql-website/get_started.webp differ diff --git a/examples/official-site/your-first-sql-website/get_started_linux.webp b/examples/official-site/your-first-sql-website/get_started_linux.webp new file mode 100644 index 00000000..1e9ce0e2 Binary files /dev/null and b/examples/official-site/your-first-sql-website/get_started_linux.webp differ diff --git a/examples/official-site/your-first-sql-website/get_started_macos.webp b/examples/official-site/your-first-sql-website/get_started_macos.webp new file mode 100644 index 00000000..91feae47 Binary files /dev/null and b/examples/official-site/your-first-sql-website/get_started_macos.webp differ diff --git a/examples/official-site/your-first-sql-website/get_started_windows.webp b/examples/official-site/your-first-sql-website/get_started_windows.webp new file mode 100644 index 00000000..175b4a43 Binary files /dev/null and b/examples/official-site/your-first-sql-website/get_started_windows.webp differ diff --git a/examples/official-site/your-first-sql-website/hello-world.png b/examples/official-site/your-first-sql-website/hello-world.png new file mode 100644 index 00000000..f587b26d Binary files /dev/null and b/examples/official-site/your-first-sql-website/hello-world.png differ diff --git a/examples/official-site/your-first-sql-website/index.sql b/examples/official-site/your-first-sql-website/index.sql index 5953d501..fe380cdc 100644 --- a/examples/official-site/your-first-sql-website/index.sql +++ b/examples/official-site/your-first-sql-website/index.sql @@ -1,52 +1,87 @@ select 'http_header' as component, - 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; - -select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; + 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", + '; rel="canonical"' as "Link"; set os = COALESCE($os, case when sqlpage.header('user-agent') like '%windows%' then 'windows' when sqlpage.header('user-agent') like '%x11; linux%' then 'linux' + when sqlpage.header('user-agent') like '%x11; ubuntu; linux%' then 'linux' + when sqlpage.header('user-agent') like '%x11; debian; linux%' then 'linux' when sqlpage.header('user-agent') like '%macintosh%' then 'macos' else 'any' end); +-- Fetch the page title and header from the database +select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( + 'title', 'SQL to Website - Tutorial', + 'description', 'Convert your SQL database into a website in minutes. In this 5-minute guide, we will create a simple website from scratch, and learn the basics of SQLPage.' +)) as properties +FROM example WHERE component = 'shell' LIMIT 1; + +SET req = '{ + "url": "https://api.github.com/repos/sqlpage/SQLPage/releases/latest", + "timeout_ms": 200 +}'; +SET api_results = sqlpage.fetch_with_meta($req); +SET sqlpage_version = COALESCE(json_extract($api_results, '$.body.tag_name'), ''); + SELECT 'hero' as component, 'Your first SQL Website' as title, - 'Let''s create your first website in SQL together, from downloading SQLPage to connecting it to your database, to making a web app' as description, - 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Backlit_keyboard.jpg/1024px-Backlit_keyboard.jpg' as image, - 'https://github.com/lovasoa/SQLpage/releases'|| case - when $os = 'windows' then '/latest/download/sqlpage-windows.zip' - when $os = 'linux' then '/latest/download/sqlpage-linux.tgz' - else '' - end as link, - 'Download SQLPage' || case - when $os = 'windows' then ' for Windows' - when $os = 'linux' then ' for Linux' - else '' - end as link_text; + '[SQLPage](/) is a free tool for building data-driven apps quickly. + +Let''s create a simple website with a database from scratch, to learn SQLPage basics.' as description_md, + case $os + when 'linux' then 'get_started_linux.webp' + when 'macos' then 'get_started_macos.webp' + when 'windows' then 'get_started_windows.webp' + else 'get_started.webp' + end as image, + CASE $os + WHEN 'macos' THEN '#download' + WHEN 'windows' THEN 'https://github.com/sqlpage/SQLPage/releases/latest/download/sqlpage-windows.zip' + WHEN 'linux' THEN 'https://github.com/sqlpage/SQLPage/releases/latest/download/sqlpage-linux.tgz' + ELSE 'https://github.com/sqlpage/SQLPage/releases' + END AS link, + CASE $os + WHEN 'macos' THEN CONCAT('Install SQLPage ', $sqlpage_version, ' using Homebrew') + WHEN 'windows' THEN CONCAT('Download SQLPage ', $sqlpage_version, ' for Windows') + WHEN 'linux' THEN CONCAT('Download SQLPage ', $sqlpage_version, ' for Linux') + ELSE CONCAT('Download SQLPage ', $sqlpage_version) + END AS link_text; SELECT 'alert' as component, 'Afraid of the setup ? Do it the easy way !' as title, 'mood-happy' as icon, 'teal' as color, - 'You don’t want to have anything to do with scary hacker things ? - You can use a preconfigured SQLPage hosted on our servers, and **never have to configure a server** yourself.' as description_md, - 'https://replit.com/@pimaj62145/SQLPage#index.sql' AS link, - 'Try SQLPage from your browser' as link_text; -select 'https://datapage.app' as link, 'Host your app on our servers' as title, 'teal' as color; + 'You don’t want to install anything on your computer ? + You can use a preconfigured SQLPage hosted on our servers, and get your app online in minutes, without **ever having to configure a server** yourself.' as description_md, + 'https://datapage.app' AS link, + 'Host your app on our servers' as link_text; +select 'https://editor.datapage.app' as link, 'Try SQLPage from your browser' as title, 'teal' as color; + SELECT 'alert' as component, 'Do you prefer videos ?' as title, 'brand-youtube' as icon, 'purple' as color, - 'I made a video to introduce you to SQLPage. You can watch it on YouTube. The video covers everything from the underlying technology to the philosophy behind SQLPage to the actual steps to create your first website.' as description_md, + 'We made videos to introduce you to SQLPage. You can watch them on YouTube. The videos cover everything from the underlying technology to the philosophy behind SQLPage to the actual steps to create your first website.' as description_md, 'https://www.youtube.com/watch?v=9NJgH_-zXjY' AS link, 'Watch the introduction video' as link_text; +select 'https://www.youtube.com/watch?v=6D5D10v18b0&list=PLTue_qIAHxActQnLn_tHWZUNXziZTeraB' as link, 'Tutorial video series' as title; + +select 'text' as component, + sqlpage.read_file_as_text( + printf('your-first-sql-website/tutorial-install-%s.md', + case + when $os = 'windows' then 'windows' + when $os = 'macos' then 'macos' + else 'any' + end + ) + ) as contents_md, + true as article, + 'download' as id; -select 'text' as component, sqlpage.read_file_as_text(printf('your-first-sql-website/tutorial-install-%s.md', - case - when $os = 'windows' then 'windows' - when $os = 'macos' then 'macos' - else 'any' - end -)) as contents_md, 'download' as id; -select 'text' as component, sqlpage.read_file_as_text('your-first-sql-website/tutorial.md') as contents_md; \ No newline at end of file +select 'text' as component, + sqlpage.read_file_as_text('your-first-sql-website/tutorial.md') as contents_md, + true as article, + 'tutorial' as id; diff --git a/examples/official-site/your-first-sql-website/migrations.md b/examples/official-site/your-first-sql-website/migrations.md index 68698af9..7d1822c1 100644 --- a/examples/official-site/your-first-sql-website/migrations.md +++ b/examples/official-site/your-first-sql-website/migrations.md @@ -143,4 +143,4 @@ Best migrations on your evolving database journey! 👋 --- -Article written by [Matthew Larkin](https://github.com/matthewlarkin) for [SQLPage](https://sql.ophir.dev/). \ No newline at end of file +Article written by [Matthew Larkin](https://github.com/matthewlarkin) for [SQLPage](https://sql-page.com/). \ No newline at end of file diff --git a/examples/official-site/your-first-sql-website/migrations.sql b/examples/official-site/your-first-sql-website/migrations.sql index 5521da91..2d5ce3c6 100644 --- a/examples/official-site/your-first-sql-website/migrations.sql +++ b/examples/official-site/your-first-sql-website/migrations.sql @@ -4,4 +4,6 @@ select 'http_header' as component, select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; -- Article by Matthew Larkin -select 'text' as component, sqlpage.read_file_as_text('your-first-sql-website/migrations.md') as contents_md; +select 'text' as component, + sqlpage.read_file_as_text('your-first-sql-website/migrations.md') as contents_md, + true as article; diff --git a/examples/official-site/your-first-sql-website/nginx.md b/examples/official-site/your-first-sql-website/nginx.md index 4ce110a6..9e156135 100644 --- a/examples/official-site/your-first-sql-website/nginx.md +++ b/examples/official-site/your-first-sql-website/nginx.md @@ -65,11 +65,31 @@ sudo systemctl reload nginx Your SQLPage instance is now hosted behind a reverse proxy using NGINX. You can access it by visiting `http://example.com`. + +### Streaming-friendly proxy settings + +SQLPage streams HTML by default so the browser can render results while the database is still sending rows. +If you have slow SQL queries (you shouldn't), you can add the following directive to your location block: + +```nginx +proxy_buffering off; +``` + +That will allow users to start seeing the top of your pages faster, +but will increase the load on your SQLPage server, and reduce the amount of users you can serve concurrently. + +Refer to the official documentation for [proxy buffering](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering), [gzip](https://nginx.org/en/docs/http/ngx_http_gzip_module.html), and [chunked transfer](https://nginx.org/en/docs/http/ngx_http_core_module.html#chunked_transfer_encoding) when tuning these values. + +When SQLPage sits behind a reverse proxy, set `compress_responses` to `false` [in `sqlpage.json`](https://github.com/sqlpage/SQLPage/blob/main/configuration.md) so that NGINX compresses once at the edge. + ### URL Rewriting URL rewriting is a powerful feature that allows you to manipulate URLs to make them more readable, search-engine-friendly, and easy to maintain. In this section, we will cover how to use URL rewriting with SQLPage. +Note that for basic URL rewriting, you can use a simple [`404.sql`](/your-first-sql-website/custom_urls.sql) file to handle custom URLs. +However, for more complex rewriting rules, you can use NGINX's `rewrite` directive. + #### Example: Rewriting `/products/$id` to `/products.sql?id=$id` Let's say you want your users to access product details using URLs like `/products/123` instead of `/products.sql?id=123`. This can be achieved using the `rewrite` directive in NGINX. @@ -322,4 +342,12 @@ server { proxy_set_header Host $host; } } -``` \ No newline at end of file +``` + +# Example NGINX configuration for SQLPage + +You can find a fully working example of an NGINX configuration for SQLPage +illustrating all the features described in this guide +in the [examples/nginx](https://github.com/sqlpage/SQLPage/tree/main/examples/nginx) +directory of the SQLPage Github repository. +The example uses Docker and docker-compose to run NGINX and SQLPage. \ No newline at end of file diff --git a/examples/official-site/your-first-sql-website/not_found.jpg b/examples/official-site/your-first-sql-website/not_found.jpg new file mode 100644 index 00000000..2d001755 Binary files /dev/null and b/examples/official-site/your-first-sql-website/not_found.jpg differ diff --git a/examples/official-site/your-first-sql-website/sql.webp b/examples/official-site/your-first-sql-website/sql.webp new file mode 100644 index 00000000..2139578b Binary files /dev/null and b/examples/official-site/your-first-sql-website/sql.webp differ diff --git a/examples/official-site/your-first-sql-website/tutorial-install-any.md b/examples/official-site/your-first-sql-website/tutorial-install-any.md index bbd47ba1..d91533b2 100644 --- a/examples/official-site/your-first-sql-website/tutorial-install-any.md +++ b/examples/official-site/your-first-sql-website/tutorial-install-any.md @@ -1,21 +1,25 @@ -# Download SQLPage: the SQL website framework +# Download SQLPage -SQLPage is a small executable file that will take requests to your website, execute the SQL files you write, +SQLPage is a small single-file program that will +execute the SQL files you write, and render the database responses as nice web pages. -[Download the latest SQLPage](https://github.com/lovasoa/SQLpage/releases) for your operating system. +If you have already downloaded SQLPage, +you can skip this step and [start writing your website](#tutorial). + +[Download the latest SQLPage](https://github.com/sqlpage/SQLPage/releases) for your operating system. In the _release assets_ section, you will find files named `sqlpage-windows.zip`, `sqlpage-linux.tgz`, and `sqlpage-macos.tgz`. Download the one that corresponds to your operating system, and extract the executable file from the archive. > **Note**: On Mac OS, Apple blocks the execution of downloaded files by default. The easiest way to run SQLPage is to use [Homebrew](https://brew.sh). > **Note**: Advanced users can alternatively install SQLPage using: -> - [docker](https://hub.docker.com/repository/docker/lovasoa/sqlpage/general) (docker images are also available for ARM, making it easy to run SQLPage on a Raspberry Pi, for example), +> - [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general) (docker images are also available for ARM, making it easy to run SQLPage on a Raspberry Pi, for example), > - [brew](https://formulae.brew.sh/formula/sqlpage) (the easiest way to install SQLPage on Mac OS), > - [nix](https://search.nixos.org/packages?channel=unstable&show=sqlpage) (declarative package management for reproducible deployments), > - [scoop](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116) (a command-line installer for Windows), > - or [cargo](https://crates.io/crates/sqlpage) (the Rust package manager). -You can also find the source code of SQLPage on [GitHub](https://github.com/lovasoa/SQLpage), [install rust](https://www.rust-lang.org/tools/install) on your computer, and compile it yourself with `cargo install sqlpage`. +You can also find the source code of SQLPage on [GitHub](https://github.com/sqlpage/SQLPage), [install rust](https://www.rust-lang.org/tools/install) on your computer, and compile it yourself with `cargo install sqlpage`. See the instructions for [MacOS](?os=macos#download), or for [Windows](?os=windows#download). diff --git a/examples/official-site/your-first-sql-website/tutorial-install-macos.md b/examples/official-site/your-first-sql-website/tutorial-install-macos.md index 2bb68df3..d020ee34 100644 --- a/examples/official-site/your-first-sql-website/tutorial-install-macos.md +++ b/examples/official-site/your-first-sql-website/tutorial-install-macos.md @@ -10,7 +10,8 @@ sqlpage ``` > **Note**: Advanced users can alternatively install SQLPage using -> [docker](https://hub.docker.com/repository/docker/lovasoa/sqlpage/general), +> [the precompiled binaries](https://github.com/sqlpage/SQLPage/releases/latest), +> [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general), > [nix](https://search.nixos.org/packages?channel=unstable&show=sqlpage), > or [cargo](https://crates.io/crates/sqlpage). diff --git a/examples/official-site/your-first-sql-website/tutorial-install-windows.md b/examples/official-site/your-first-sql-website/tutorial-install-windows.md index 992b8672..19767d6f 100644 --- a/examples/official-site/your-first-sql-website/tutorial-install-windows.md +++ b/examples/official-site/your-first-sql-website/tutorial-install-windows.md @@ -3,11 +3,11 @@ SQLPage offers a small executable file (`sqlpage.exe`) that will take requests to your website, execute the SQL files you write, and render the database responses as nice web pages. -[Download the latest SQLPage for Windows](https://github.com/lovasoa/SQLpage/releases/latest/download/sqlpage-windows.zip). +[Download the latest SQLPage for Windows](https://github.com/sqlpage/SQLPage/releases/latest/download/sqlpage-windows.zip). Download the file, and extract the executable file from the zip archive. > **Note**: Advanced users can alternatively install SQLPage using -> [docker](https://hub.docker.com/repository/docker/lovasoa/sqlpage/general), +> [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general), > [scoop](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116), > or [cargo](https://crates.io/crates/sqlpage). diff --git a/examples/official-site/your-first-sql-website/tutorial.md b/examples/official-site/your-first-sql-website/tutorial.md index 797bd050..582a9463 100644 --- a/examples/official-site/your-first-sql-website/tutorial.md +++ b/examples/official-site/your-first-sql-website/tutorial.md @@ -3,37 +3,43 @@ Create a folder on your computer where you will store all contents related to your sql website. In the rest of this tutorial, we will call this folder the **root folder** of your website. -Open the file you downloaded above, and place `sqlpage.bin` (if you are on linux or Mac OS) -or `sqlpage.exe` at the root of the folder. +- On **Windows**, place the `sqlpage.exe` you downloaded above at the root of the folder. Then double-click the `sqlpage.exe` file to start the server. +- On **Linux**, place `sqlpage.bin` at the root of the folder. Then open a terminal, cd to the root folder of your website, and run `./sqlpage.bin` to start the server. +- On **Mac OS**, if you installed SQLPage using Homebrew, then you do not need to place anything at the root of the folder. Open Terminal, cd to the root folder of your website, and type `sqlpage` to start the server. -Then launch the `sqlpage.bin` executable file you just downloaded in a terminal from this folder. +![screenshot for the sql website setup on linux](first-sql-website-launch.png) -![screenshot for the sql website setup](first-sql-website-launch.png) - -You should see a message in your terminal that includes the sentence `accessible from the network, and locally on http://localhost:8080` +You should see a message in your terminal telling you that SQLPage is ready, and giving you the address of your website. You can open your website locally by visiting [`http://localhost:8080`](http://localhost:8080) -# Your website’s first SQL file +SQLPage should have automatically created a folder called `sqlpage` with a SQLite database file named `sqlpage.db`. This is your website's default database - don't worry, we'll learn how to connect to other databases like PostgreSQL, MySQL, or SQL Server later! + +# Your website's first SQL file In the root folder of your SQLPage website, create a new SQL file called `index.sql`. Open it in a text editor that supports SQL syntax highlighting (I recommend [VSCode](https://code.visualstudio.com/)). -The `index.sql` file will be executed every time a visitor opens your website's home page. -You can use it to retrieve data from your database and define how it should be displayed to your visitors. +The `index.sql` file will be executed every time a visitor opens your website's home page, and the results will be displayed to the visitor +using the components you specify in the file. -As an example, let's start with a simple `index.sql` that displays a list of popular websites: +Let's start with a simple `index.sql` that displays a list of popular websites: ```sql -SELECT 'list' AS component, 'Popular websites' AS title; +SELECT 'list' AS component, + 'Popular websites' AS title; -SELECT 'Hello' AS title, 'world' AS description, 'https://wikipedia.org' AS link; +SELECT 'Hello' AS title, + 'world' AS description, + 'https://wikipedia.org' AS link; ``` +![screenshot of the first sql website](hello-world.png) + The first line of the file defines the component that will be used to display the data, and properties of that component. -In this case, we use the [`list` component](/documentation.sql?component=list#component) to display a list of items. +In this case, we use the [`list` component](/component.sql?component=list) to display a list of items. The second line defines the data that will populate the component. -All the components you can use and their properties are documented in [SQLPage's online documentation](https://sql.ophir.dev/documentation.sql). +All the components you can use and their properties are documented in [SQLPage's online documentation](https://sql-page.com/documentation.sql). # Your database schema @@ -54,6 +60,9 @@ CREATE TABLE users ( ); ``` +If you need to quickly test a database schema and associated queries online, +before making any change to your database, I can recommend [sqliteonline.com](https://sqliteonline.com/) (which actually also works with Postgres, MySQL, and SQL Server). + Please read our [**introduction to database migrations**](./migrations.sql) to learn how to maintain your database schema in the long term. @@ -73,19 +82,23 @@ Here is an example `sqlpage.json` file: { "database_url": "sqlite://:memory:" } ``` -This will tell SQLPage to use an in-memory SQLite database instead of the default file-based database. -All your data will be lost when you stop the SQLPage server, but it is useful for quickly testing and iterating on your database schema. +This will tell SQLPage to use an in-memory SQLite database instead of the default file-based database. While this means all changes to the database will be lost when you stop the SQLPage server, it's useful for quickly testing and iterating on your database schema. +If you then deploy your website online using a service like [DataPage.app](https://datapage.app), it will automatically use a persisted database instead. + +Later, when you want to deploy your website online, you can switch back to a persistent database like -Later, when you want to deploy your website online, you can switch back to a persisted database like +- a SQLite file with `sqlite://your-database-file.db` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/sqlite/struct.SqliteConnectOptions.html)), +- a PostgreSQL-compatible server with `postgres://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/postgres/struct.PgConnectOptions.html)), +- a MySQL-compatible server with `mysql://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/mysql/struct.MySqlConnectOptions.html)), +- a Microsoft SQL Server with `mssql://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/mssql/struct.MssqlConnectOptions.html#method.from_str), [note about named instances](https://github.com/sqlpage/SQLPage/issues/92)), +- any ODBC-compatible database like DuckDB, ClickHouse, Databricks, Snowflake, BigQuery, Oracle, Db2, and many more. See [ODBC database connection instructions](https://github.com/sqlpage/SQLPage#odbc-setup). -- a SQLite file with `sqlite://your-database-file.db` ([see options](https://docs.rs/sqlx/0.6.3/sqlx/sqlite/struct.SqliteConnectOptions.html#main-content)), -- a PostgreSQL-compatible server with `postgres://user:password@host/database` ([see options](https://www.postgresql.org/docs/15/libpq-connect.html#id-1.7.3.8.3.6)), -- a MySQL-compatible server with `mysql://user:password@host/database` ([see options](https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html)), -- a Microsoft SQL Server with `mssql://user:password@host/database` ([see options](https://docs.rs/sqlx-oldapi/latest/sqlx_oldapi/mssql/struct.MssqlConnectOptions.html), [note about named instances](https://github.com/lovasoa/SQLpage/issues/92)), +> If `user` or `password` **contains special characters**, you should [**percent-encode**](https://en.wikipedia.org/wiki/Percent-encoding) them. +> +> For instance, a SQL Server database named `db` running on `localhost` port `1433` with the username `funny:user` and the password `p@ssw0rd` would be represented as +> `mssql://funny%3Auser:p%40ssw0rd@localhost:1433/db`. -If `user` or `password` contains special characters, you should [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) them. -For instance, a SQL Server database named `db` running on `localhost` port `1433` with the username `funny:user` and the password `p@ssw0rd` would be represented as `mssql://funny%3Auser:p%40ssw0rd@localhost:1433/db`. -For more information about the properties that can be set in sqlpage.json, see [SQLPage's configuration documentation](https://github.com/lovasoa/SQLpage/blob/main/configuration.md#configuring-sqlpage) +For more information about the properties that can be set in sqlpage.json, see [SQLPage's configuration documentation](https://github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage) ![screenshot for the full sql website folder organisation](full-website.png) @@ -93,18 +106,19 @@ For more information about the properties that can be set in sqlpage.json, see [ ### Displaying a form -Let’s create a form to let our users insert data into our database. Add the following code to your `index.sql` file: +Let's create a form to let our users insert data into our database. Add the following code to your `index.sql` file: ```sql SELECT 'form' AS component, 'Add a user' AS title; SELECT 'Username' as name, TRUE as required; ``` -The snippet above uses the [`form` component](https://sql.ophir.dev/documentation.sql?component=form#component) to display a form on your website. +The first SELECT statement opens the [`form` component](https://sql-page.com/component.sql?component=form). +The second SELECT statement adds a field to the form. Since we do not specify a `type`, it will be a text field. The label displayed above the field will be the same as its name by default. ### Handling form submission -Nothing happens when you submit the form at the moment. Let’s fix that. +Nothing happens when you submit the form at the moment. Let's fix that. Add the following below the previous code: ```sql @@ -119,16 +133,30 @@ It uses a `WHERE` clause to make sure that the `INSERT` statement is only execut The `:Username` parameter is set to `NULL` when you initially load the page, and then SQLPage automatically sets it to the value from the text field when the user submits the form. +#### Parameters + There are two types of parameters you can use in your SQL queries: -- `:ParameterName` is a [POST]() parameter. It is set to the value of the field with the corresponding `name` in a form. If no form was submitted, it is set to `NULL`. -- `$ParameterName` works the same as `:ParameterName`, but it can also be set through a [query parameter](https://en.wikipedia.org/wiki/Query_string) in the URL. - If you add `?x=1&y=2` to the end of the URL of your page, `$x` will be set to the string `'1'` and `$y` will be set to the string `'2'`. - If a query parameter was not provided, it is set to `NULL`. +- **URL parameters** like **`$ParameterName`**. If you add `?x=1&y=2` to the end of the URL of your page, `$x` will be set to the string `'1'` and `$y` will be set to the string `'2'`. This is useful to create links with parameters. For instance, if you have a database of products, you can create a link to a product page with the URL `product?product_id=12` (or `product.sql?product_id=12` - both work). Then, in the `product.sql` file, you can use the `$product_id` variable to get the product with the corresponding ID from your database. URL parameters are also sometimes called *query parameters*, or *GET parameters*. +- **Form parameters** like **`:ParameterName`**. They refer to the value of the field with the corresponding `name` entered by the user in a [form](/component.sql?component=form). If no form was submitted, it is set to `NULL`. Form parameters are also sometimes called *POST parameters*. + +> Note: Currently, if a `$parameter` is not present in the URL, it is first looked for in the form parameters. If it is not found there either, it is set to `NULL`. Please do not rely on this behavior, as it may change in the future. + +You can also set parameters yourself at any point in your SQL files in order to reuse +their value in several places, using the `SET ParameterName = value` syntax. +For instance, we could use the following code to save the username in uppercase: + +```sql +SET Username = UPPER(:Username); +INSERT INTO users (name) VALUES ($Username); +``` + +### Displaying data from our database -### Displaying contents from the database +Now, users are present in our database, but we can't see them. +Let's see how to use data from our database to populate a [list](/component.sql?component=list) component, in order to display the list of users. -Now, users are present in our database, but we can’t see them. Let’s fix that by adding the following code to our `index.sql` file: +Add the following code to your `index.sql` file: ```sql SELECT 'list' AS component, 'Users' AS title; @@ -137,13 +165,13 @@ SELECT name AS title, CONCAT(name, ' is a user on this website.') as descriptio ### Your first SQLPage website is ready! -You can view [the full source code for this example on Github](https://github.com/lovasoa/SQLpage/tree/main/examples/simple-website-example) +You can view [the full source code for this example on Github](https://github.com/sqlpage/SQLPage/tree/main/examples/simple-website-example) Here is a screenshot of the final result: ![final result](final-result.png) -To go further, have a look at [the examples section of our Github repository](https://github.com/lovasoa/SQLpage/tree/main/examples). +To go further, have a look at [the examples](../examples/). # Deploy your SQLPage website online @@ -157,14 +185,15 @@ Just create an account, and follow the instructions to upload your website to ou If you prefer to host your website yourself, you can use a cloud provider or a VPS provider. You will need to: - Configure domain name resolution to point to your server - Open the port you are using (8080 by default) in your server's firewall -- [Setup docker](https://github.com/lovasoa/SQLpage?tab=readme-ov-file#with-docker) or another process manager such as [systemd](https://github.com/lovasoa/SQLpage/blob/main/sqlpage.service) to start SQLPage automatically when your server boots and to keep it running -- Optionnally, [setup a reverse proxy](nginx.sql) to avoid exposing SQLPage directly to the internet -- Optionnally, setup a TLS certificate to enable HTTPS -- Configure connection to a cloud database or a database running on your server in [`sqlpage.json`](https://github.com/lovasoa/SQLpage/blob/main/configuration.md#configuring-sqlpage) +- [Setup docker](https://github.com/sqlpage/SQLPage?tab=readme-ov-file#with-docker) or another process manager such as [systemd](https://github.com/sqlpage/SQLPage/blob/main/sqlpage.service) to start SQLPage automatically when your server boots and to keep it running +- Optionally, [setup a reverse proxy](nginx.sql) to avoid exposing SQLPage directly to the internet +- Optionally, setup a TLS certificate to enable HTTPS +- Configure connection to a cloud database or a database running on your server in [`sqlpage.json`](https://github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage) # Go further - Check out [learnsqlpage.com](https://learnsqlpage.com) by Nick Antonaccio for an in-depth tutorial with many examples - Read the [SQLPage documentation](/documentation.sql) to learn about all the components available in SQLPage -- Join the [SQLPage community](https://github.com/lovasoa/SQLpage/discussions) to ask questions and share your projects +- Read about [SQLPage's extensions to SQL](/extensions-to-sql) for a specification of the SQL syntax you can use in SQLPage, the data types used when exchanging data with the browser and with the database, a clear explanation of how *SQLPage variables* and *SQLPage functions* work. +- Join the [SQLPage community](https://github.com/sqlpage/SQLPage/discussions) to ask questions and share your projects - If you like videos better, check this series that shows how to build and deploy your app from scratch [SQLPage on Youtube](https://www.youtube.com/playlist?list=PLTue_qIAHxAf9fEjBY2CN0N_5XOiffOk_) \ No newline at end of file diff --git a/examples/plots tables and forms/index.sql b/examples/plots tables and forms/index.sql index 07c6ee5b..0a41f0a3 100644 --- a/examples/plots tables and forms/index.sql +++ b/examples/plots tables and forms/index.sql @@ -8,7 +8,7 @@ select 'shell' as component, -- Making a web page with SQLPage works by using a set of predefined "components" -- and filling them with contents from the results of your SQL queries -select 'hero' as component, -- We select a component. The documentation for each component can be found on https://sql.ophir.dev/documentation.sql +select 'hero' as component, -- We select a component. The documentation for each component can be found on https://sql-page.com/documentation.sql 'It works !' as title, -- 'title' is top-level parameter of the 'hero' component 'If you can see this, then SQLPage is running correctly on your server. Congratulations! ' as description; -- Properties can be textual, numeric, or booleans @@ -21,7 +21,7 @@ SELECT 'text' as component, -- We can switch to another component at any time ju -- and a property called "center" that we use to center the text SELECT 'In order to get started ' as contents; select 'visit SQLPage''s website' as contents, - 'https://sql.ophir.dev/' as link, + 'https://sql-page.com/' as link, true as italics; SELECT '. You can replace this page''s contents by creating a file named ' as contents; SELECT 'index.sql' as contents, true as italics; @@ -31,7 +31,7 @@ SELECT 'Alternatively, you can create a table called sqlpage_files in your datab -- The text component also support rich text using the markdown syntax with the property "contents_md" SELECT ' ## Rich text -You can use markdown syntax in SQLPage to make your text **bold**, *italic*, or even [add links](https://sql.ophir.dev/). +You can use markdown syntax in SQLPage to make your text **bold**, *italic*, or even [add links](https://sql-page.com/). ' as contents_md; select 'text' as component, @@ -39,7 +39,7 @@ select 'text' as component, -- We can switch to another component at any time just with a select statement. -- Let's draw a chart select 'chart' as component, -- selecting a different component - 'Revenue per country' as title, -- setting the component's top-level properties. The documentation for each component's properties can be found on https://sql.ophir.dev/documentation.sql + 'Revenue per country' as title, -- setting the component's top-level properties. The documentation for each component's properties can be found on https://sql-page.com/documentation.sql 'bar' as type, 'time' as xtitle, 'price' as ytitle, @@ -112,7 +112,7 @@ FROM nums as a, nums as b WHERE -- The powerful thing is here $x IS NULL OR -- The syntax $x allows us to extract the value 'a' when the URL ends with '?x=a'. It will be null if the URL does not contain '?x=' - b.x = $x::DECIMAL + b.x = CAST($x AS DECIMAL) ORDER BY a.x, b.x; -- So when we click the card for "a times b", we will reload the page, and display only the multiplication table of a --------------------------- @@ -164,4 +164,4 @@ select 'checkbox' as type, select 'debug' as component; select $x as x, :"First Name" as firstName, - :checks as checks; \ No newline at end of file + :checks as checks; diff --git a/examples/read-and-set-http-cookies/sqlpage/sqlpage.json b/examples/read-and-set-http-cookies/sqlpage/sqlpage.json index 78995eb2..268bd02b 100644 --- a/examples/read-and-set-http-cookies/sqlpage/sqlpage.json +++ b/examples/read-and-set-http-cookies/sqlpage/sqlpage.json @@ -1,3 +1,3 @@ { - "database_url": "sqlite://:memory:" -} \ No newline at end of file + "database_url": "sqlite://:memory:" +} diff --git a/examples/rich-text-editor/README.md b/examples/rich-text-editor/README.md new file mode 100644 index 00000000..eb0424cf --- /dev/null +++ b/examples/rich-text-editor/README.md @@ -0,0 +1,6 @@ +# SQLPage rich text editor + +This demo shows how to build an application where users can +input and safely store rich text (with titles, bold, italics, and images). + +![image](https://github.com/user-attachments/assets/47e43ee2-b7cb-4d72-a244-4a8885b51577) diff --git a/examples/rich-text-editor/create_blog_post.sql b/examples/rich-text-editor/create_blog_post.sql new file mode 100644 index 00000000..01289125 --- /dev/null +++ b/examples/rich-text-editor/create_blog_post.sql @@ -0,0 +1,5 @@ +insert into blog_posts (title, content) +values (:title, :content) +returning + 'redirect' as component, + 'post?id=' || id as link; \ No newline at end of file diff --git a/examples/rich-text-editor/edit.sql b/examples/rich-text-editor/edit.sql new file mode 100644 index 00000000..ff44c066 --- /dev/null +++ b/examples/rich-text-editor/edit.sql @@ -0,0 +1,35 @@ +insert or replace into blog_posts (id, title, content) +select $id, :title, :content +where $id is not null and :title is not null and :content is not null +returning 'redirect' as component, 'post?id=' || $id as link; + +select 'shell' as component, + 'Edit blog post' as title, + '/rich_text_editor.js' as javascript_module; + + +select 'form' as component, 'Update' as validate; + +with post as ( + select title, content + from blog_posts + where id = $id +), +fields as ( + select json_object( + 'name', 'title', + 'label', 'Blog post title', + 'value', title + ) as props + from post + union all + select json_object( + 'name', 'content', + 'type', 'textarea', + 'label', 'Your blog post here', + 'value', content, + 'required', true + ) + from post +) +select 'dynamic' as component, json_group_array(props) as properties from fields; \ No newline at end of file diff --git a/examples/rich-text-editor/index.sql b/examples/rich-text-editor/index.sql new file mode 100644 index 00000000..1ca5dafa --- /dev/null +++ b/examples/rich-text-editor/index.sql @@ -0,0 +1,18 @@ +select 'shell' as component, + 'Rich text editor' as title, + '/rich_text_editor.js' as javascript_module; + + +select 'form' as component, + 'Create a new blog post' as title, + 'create_blog_post' as action, + 'Create' as validate; + +select 'title' as name, 'Blog post title' as label, 'My new post' as value; +select 'content' as name, 'textarea' as type, 'Your blog post here' as label, 'Your blog post here' as value, true as required, $disabled is not null as disabled; + +select 'list' as component, + 'Blog posts' as title; + +select title, sqlpage.link('post', json_object('id', id)) as link +from blog_posts; diff --git a/examples/rich-text-editor/post.sql b/examples/rich-text-editor/post.sql new file mode 100644 index 00000000..1e8f85d0 --- /dev/null +++ b/examples/rich-text-editor/post.sql @@ -0,0 +1,16 @@ +select 'shell' as component, + title +from blog_posts +where id = $id; + +select 'text' as component, + true as article, + content as contents_md +from blog_posts +where id = $id; + +select 'list' as component; +select + 'Edit' as title, + 'pencil' as icon, + 'edit?id=' || $id as link; diff --git a/examples/rich-text-editor/rich_text_editor.js b/examples/rich-text-editor/rich_text_editor.js new file mode 100644 index 00000000..33593343 --- /dev/null +++ b/examples/rich-text-editor/rich_text_editor.js @@ -0,0 +1,903 @@ +import { fromMarkdown } from "https://esm.sh/mdast-util-from-markdown@2.0.0"; +import { toMarkdown as mdastUtilToMarkdown } from "https://esm.sh/mdast-util-to-markdown@2.1.2"; +import Quill from "https://esm.sh/quill@2.0.3"; + +/** + * @typedef {Object} QuillAttributes + * @property {boolean} [bold] - Whether the text is bold. + * @property {boolean} [italic] - Whether the text is italic. + * @property {string} [link] - URL if the text is a link. + * @property {number} [header] - Header level (1-3). + * @property {string} [list] - List type ('ordered' or 'bullet'). + * @property {boolean} [blockquote] - Whether the text is in a blockquote. + * @property {string} [code-block] - Code language if in a code block. + * @property {string} [alt] - Alt text for images. + */ + +/** + * @typedef {Object} QuillOperation + * @property {string|Object} [insert] - Content to insert (string or object with image URL). + * @property {number} [delete] - Number of characters to delete. + * @property {number} [retain] - Number of characters to retain. + * @property {QuillAttributes} [attributes] - Formatting attributes. + */ + +/** + * @typedef {Object} QuillDelta + * @property {Array} ops - Array of operations in the delta. + */ + +/** + * Converts Quill Delta object to a Markdown string using mdast. + * @param {QuillDelta} delta - Quill Delta object (https://quilljs.com/docs/delta/). + * @returns {string} - Markdown representation. + */ +function deltaToMarkdown(delta) { + const mdastTree = deltaToMdast(delta); + const options = { + bullet: "*", + listItemIndent: "one", + handlers: {}, + unknownHandler: (node) => { + console.warn(`Unknown node type encountered: ${node.type}`, node); + return false; + }, + }; + return mdastUtilToMarkdown(mdastTree, options); +} + +/** + * Creates a div to replace the textarea and prepares it for Quill. + * @param {HTMLTextAreaElement} textarea - The original textarea. + * @returns {HTMLDivElement} - The div element created for the Quill editor. + */ +function createAndReplaceTextarea(textarea) { + const editorDiv = document.createElement("div"); + editorDiv.className = "mb-3"; + editorDiv.style.height = "250px"; + + const label = textarea.closest("label"); + if (!label) { + textarea.parentNode.insertBefore(editorDiv, textarea); + } else { + label.parentNode.insertBefore(editorDiv, label.nextSibling); + } + // Hide the original textarea, but keep it focusable for validation + textarea.style = "transform: scale(0); position: absolute; opacity: 0;"; + return editorDiv; +} + +/** + * Returns the toolbar options array configured for Markdown compatibility. + * @returns {Array>} - Quill toolbar options. + */ +function getMarkdownToolbarOptions() { + return [ + [{ header: 1 }, { header: 2 }, { header: 3 }], + ["bold", "italic", "code"], + ["link", "image", "blockquote", "code-block"], + [{ list: "ordered" }, { list: "bullet" }], + ["clean"], + ]; +} + +/** + * Initializes a Quill editor instance on a given div. + * @param {HTMLDivElement} editorDiv - The div element for the editor. + * @param {Array>} toolbarOptions - The toolbar configuration. + * @param {string} initialValue - The initial content for the editor. + * @returns {Quill} - The initialized Quill instance. + */ +function initializeQuillEditor( + editorDiv, + toolbarOptions, + initialValue, + readOnly, +) { + const quill = new Quill(editorDiv, { + theme: "snow", + modules: { + toolbar: toolbarOptions, + }, + readOnly: readOnly, + formats: [ + "bold", + "italic", + "link", + "header", + "list", + "blockquote", + "code", + "code-block", + "image", + ], + }); + if (initialValue) { + const delta = markdownToDelta(initialValue); + quill.setContents(delta); + } + return quill; +} + +/** + * Converts Markdown string to a Quill Delta object. + * @param {string} markdown - The markdown string to convert. + * @returns {QuillDelta} - Quill Delta representation. + */ +function markdownToDelta(markdown) { + try { + const mdastTree = fromMarkdown(markdown); + return mdastToDelta(mdastTree); + } catch (error) { + console.error("Error parsing markdown:", error); + return { ops: [{ insert: markdown }] }; + } +} + +/** + * Converts MDAST to Quill Delta. + * @param {MdastNode} tree - The MDAST tree to convert. + * @returns {QuillDelta} - Quill Delta representation. + */ +function mdastToDelta(tree) { + const delta = { ops: [] }; + if (!tree || !tree.children) return delta; + + for (const node of tree.children) { + traverseMdastNode(node, delta); + } + + return delta; +} + +/** + * Recursively traverse MDAST nodes and convert to Delta operations. + * @param {MdastNode} node - The MDAST node to process. + * @param {QuillDelta} delta - The Delta object to append operations to. + * @param {QuillAttributes} [attributes={}] - The current attributes to apply. + */ +function traverseMdastNode(node, delta, attributes = {}) { + if (!node) return; + + switch (node.type) { + case "root": + for (const child of node.children || []) { + traverseMdastNode(child, delta); + } + break; + + case "paragraph": { + for (const child of node.children || []) { + traverseMdastNode(child, delta, attributes); + } + const pLineAttributes = {}; + if (attributes.blockquote) { + pLineAttributes.blockquote = true; + } + delta.ops.push({ insert: "\n", attributes: pLineAttributes }); + break; + } + + case "heading": { + const headingContentAttributes = { ...attributes, header: node.depth }; + for (const child of node.children || []) { + traverseMdastNode(child, delta, headingContentAttributes); + } + const headingLineAttributes = { header: node.depth }; + if (attributes.blockquote) { + headingLineAttributes.blockquote = true; + } + delta.ops.push({ insert: "\n", attributes: headingLineAttributes }); + break; + } + + case "text": + delta.ops.push({ insert: node.value || "", attributes }); + break; + + case "strong": + for (const child of node.children || []) { + traverseMdastNode(child, delta, { ...attributes, bold: true }); + } + break; + + case "emphasis": + for (const child of node.children || []) { + traverseMdastNode(child, delta, { ...attributes, italic: true }); + } + break; + + case "link": + for (const child of node.children || []) { + traverseMdastNode(child, delta, { ...attributes, link: node.url }); + } + break; + + case "image": + delta.ops.push({ + insert: { image: node.url }, + attributes: { alt: node.alt || "" }, + }); + break; + + case "list": + for (const child of node.children || []) { + traverseMdastNode(child, delta, { + ...attributes, + list: node.ordered ? "ordered" : "bullet", + }); + } + break; + + case "listItem": { + // biome-ignore lint/correctness/noUnusedVariables: object destructuring with a spread + const { list, ...listItemChildrenAttributes } = attributes; + + for (const child of node.children || []) { + traverseMdastNode(child, delta, listItemChildrenAttributes); + } + + // Attributes for the listItem's newline (e.g., { list: 'bullet', blockquote: true }) + // are in `attributes` passed to this `listItem` case. + { + const lastOp = delta.ops[delta.ops.length - 1]; + if (lastOp && lastOp.insert === "\n") { + lastOp.attributes = { ...lastOp.attributes, ...attributes }; + } else { + delta.ops.push({ insert: "\n", attributes }); + } + } + break; + } + + case "blockquote": + for (const child of node.children || []) { + traverseMdastNode(child, delta, { ...attributes, blockquote: true }); + } + break; + + case "code": { + // mdast 'code' is a block + const codeBlockLineFormat = { "code-block": node.lang || true }; + if (attributes.blockquote) { + codeBlockLineFormat.blockquote = true; + } + + const textInCodeAttributes = {}; + if (attributes.blockquote) { + // Text lines also get blockquote if active + textInCodeAttributes.blockquote = true; + } + + const lines = (node.value || "").split("\n"); + for (const lineText of lines) { + delta.ops.push({ insert: lineText, attributes: textInCodeAttributes }); + delta.ops.push({ insert: "\n", attributes: codeBlockLineFormat }); + } + break; + } + + case "inlineCode": + delta.ops.push({ + insert: node.value || "", + attributes: { ...attributes, code: true }, + }); + break; + + default: + if (node.children) { + for (const child of node.children) { + traverseMdastNode(child, delta, attributes); + } + } else if (node.value) { + delta.ops.push({ insert: node.value, attributes }); + } + } +} + +/** + * Attaches a submit event listener to the form to update the hidden textarea. + * @param {HTMLFormElement|null} form - The form containing the editor. + * @param {HTMLTextAreaElement} textarea - The original (hidden) textarea. + * @param {Quill} quill - The Quill editor instance. + * @returns {void} + */ +function updateTextareaOnSubmit(form, textarea, quill) { + if (!form) { + console.warn( + "Textarea not inside a form, submission handling skipped for:", + textarea.name || textarea.id, + ); + return; + } + form.addEventListener("submit", (event) => { + const delta = quill.getContents(); + const markdownContent = deltaToMarkdown(delta); + textarea.value = markdownContent; + console.log( + `${textarea.name}:\n${markdownContent}\ntransformed from delta:\n${JSON.stringify(delta, null, 2)}`, + ); + if (textarea.required && !markdownContent) { + textarea.setCustomValidity(`${textarea.name} cannot be empty`); + quill.once("text-change", (delta) => { + textarea.value = deltaToMarkdown(delta); + textarea.setCustomValidity(""); + }); + quill.focus(); + event.preventDefault(); + } + }); +} + +/** + * Loads the Quill CSS stylesheet. + * @returns {void} + */ +function loadQuillStylesheet() { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://esm.sh/quill@2.0.3/dist/quill.snow.css"; + document.head.appendChild(link); +} + +/** + * Handles errors during editor initialization. + * @param {HTMLTextAreaElement} textarea - The textarea that failed initialization. + * @param {Error} error - The error that occurred. + * @returns {void} + */ +function handleEditorInitError(textarea, error) { + console.error("Failed to initialize Quill for textarea:", textarea, error); + textarea.style.display = ""; + const errorMsg = document.createElement("p"); + errorMsg.textContent = "Failed to load rich text editor."; + errorMsg.style.color = "red"; + textarea.parentNode.insertBefore(errorMsg, textarea.nextSibling); +} + +/** + * Sets up a single editor for a textarea. + * @param {HTMLTextAreaElement} textarea - The textarea to replace with an editor. + * @param {Array>} toolbarOptions - The toolbar configuration. + * @returns {boolean} - Whether the setup was successful. + */ +function setupSingleEditor(textarea, toolbarOptions) { + if (textarea.dataset.quillInitialized === "true") { + return false; + } + + try { + const initialValue = textarea.value; + const readOnly = textarea.readOnly || textarea.disabled; + const form = textarea.closest("form"); + const editorDiv = createAndReplaceTextarea(textarea); + const quill = initializeQuillEditor( + editorDiv, + toolbarOptions, + initialValue, + readOnly, + ); + updateTextareaOnSubmit(form, textarea, quill); + textarea.dataset.quillInitialized = "true"; + return true; + } catch (error) { + handleEditorInitError(textarea, error); + return false; + } +} + +/** + * Initializes Quill editors for all textareas in the document. + * @returns {void} + */ +function initializeEditors() { + loadQuillStylesheet(); + + const textareas = document.getElementsByTagName("textarea"); + if (textareas.length === 0) { + return; + } + + const toolbarOptions = getMarkdownToolbarOptions(); + let initializedCount = 0; + + for (const textarea of textareas) { + if (setupSingleEditor(textarea, toolbarOptions)) { + initializedCount++; + } + } + + if (initializedCount > 0) { + console.log( + `Successfully initialized Quill for ${initializedCount} textareas.`, + ); + } +} + +// MDAST conversion functions +/** + * @typedef {Object} MdastNode + * @property {string} type - The type of the node. + * @property {Array} [children] - Child nodes. + * @property {string} [value] - Text value for text nodes. + * @property {string} [url] - URL for link and image nodes. + * @property {string} [title] - Title for image nodes. + * @property {string} [alt] - Alt text for image nodes. + * @property {number} [depth] - Depth for heading nodes. + * @property {boolean} [ordered] - Whether the list is ordered. + * @property {boolean} [spread] - Whether the list is spread. + * @property {string} [lang] - Language for code blocks. + */ + +/** + * Converts a Quill Delta to a MDAST (Markdown Abstract Syntax Tree). + * @param {QuillDelta} delta - The Quill Delta to convert. + * @returns {MdastNode} - The root MDAST node. + */ +function deltaToMdast(delta) { + const mdast = createRootNode(); + /** @type {MdastNode|null} */ + let currentParagraph = null; + /** @type {MdastNode|null} */ + let currentList = null; + let textBuffer = ""; + + for (const op of delta.ops) { + if (isImageInsert(op)) { + if (!currentParagraph) { + currentParagraph = createParagraphNode(); + } + currentParagraph.children.push(createImageNode(op)); + } + if (typeof op.insert !== "string") continue; + + const text = op.insert; + const attributes = op.attributes || {}; + + // Handle newlines within text content + if (text.includes("\n") && text !== "\n") { + const lines = text.split("\n"); + + // Process all lines except the last one as complete lines + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i]; + if (line.length > 0) { + // Add text to current paragraph + if (!currentParagraph) { + currentParagraph = createParagraphNode(); + } + const nodes = createTextNodes(line, attributes); + currentParagraph.children.push(...nodes); + textBuffer = line; + } + + // Process line break + currentList = processLineBreak( + mdast, + currentParagraph, + attributes, + textBuffer, + currentList, + ); + + currentParagraph = null; + textBuffer = ""; + } + + // Add the last line to the buffer without processing the line break yet + const lastLine = lines[lines.length - 1]; + if (lastLine.length > 0) { + if (!currentParagraph) { + currentParagraph = createParagraphNode(); + } + const nodes = createTextNodes(lastLine, attributes); + currentParagraph.children.push(...nodes); + textBuffer = lastLine; + } + + continue; + } + + if (text === "\n") { + currentList = processLineBreak( + mdast, + currentParagraph, + attributes, + textBuffer, + currentList, + ); + + // Reset paragraph and buffer after processing line break + currentParagraph = null; + textBuffer = ""; + continue; + } + + // Process regular text + const nodes = createTextNodes(text, attributes); + + if (!currentParagraph) { + currentParagraph = createParagraphNode(); + } + + textBuffer += text; + currentParagraph.children.push(...nodes); + } + + if (currentParagraph) { + mdast.children.push(currentParagraph); + } + + return mdast; +} + +/** + * Creates a root MDAST node. + * @returns {MdastNode} - The root node. + */ +function createRootNode() { + return { + type: "root", + children: [], + }; +} + +/** + * Creates a paragraph MDAST node. + * @returns {MdastNode} - The paragraph node. + */ +function createParagraphNode() { + return { + type: "paragraph", + children: [], + }; +} + +/** + * Checks if an operation is an image insertion. + * @param {Object} op - The operation to check. + * @returns {boolean} - Whether the operation is an image insertion. + */ +function isImageInsert(op) { + return typeof op.insert === "object" && op.insert.image; +} + +/** + * Creates an image MDAST node. + * @param {Object} op - The operation containing the image. + * @returns {MdastNode} - The image node. + */ +function createImageNode(op) { + return { + type: "image", + url: op.insert.image, + title: op.attributes?.alt || "", + alt: op.attributes?.alt || "", + }; +} + +/** + * Creates a text MDAST node with optional formatting. + * @param {string} text - The text content. + * @param {Object} attributes - The formatting attributes. + * @returns {MdastNode[]} - The formatted text nodes. + */ +function createTextNodes(text, attributes) { + let nodes = text.split("\n").flatMap((value, i) => [ + ...(i > 0 ? [{ type: "break" }] : []), + { + type: "text", + value, + }, + ]); + + if (attributes.bold) { + nodes = [wrapNodesWith(nodes, "strong")]; + } + + if (attributes.italic) { + nodes = [wrapNodesWith(nodes, "emphasis")]; + } + + if (attributes.link) { + nodes = [{ ...wrapNodesWith(nodes, "link"), url: attributes.link }]; + } + + return nodes; +} + +/** + * Wraps a node with a formatting container. + * @param {MdastNode[]} children - The node to wrap. + * @param {string} type - The type of container. + * @returns {MdastNode} - The wrapped node. + */ +function wrapNodesWith(children, type) { + return { + type: type, + children, + }; +} + +/** + * Processes a line break in the Delta. + * @param {MdastNode} mdast - The root MDAST node. + * @param {MdastNode|null} currentParagraph - The current paragraph being built. + * @param {Object} attributes - The attributes for the line. + * @param {string} textBuffer - The text buffer for the current line. + * @param {MdastNode|null} currentList - The current list being built. + * @returns {MdastNode|null} - The updated current list. + */ +function processLineBreak( + mdast, + currentParagraph, + attributes, + textBuffer, + currentList, +) { + if (!currentParagraph) { + return handleEmptyLineWithAttributes(mdast, attributes, currentList); + } + + if (attributes.header) { + processHeaderLineBreak(mdast, textBuffer, attributes); + return null; + } + + if (attributes["code-block"]) { + processCodeBlockLineBreak(mdast, textBuffer, attributes); + return currentList; + } + + if (attributes.list) { + return processListLineBreak( + mdast, + currentParagraph, + attributes, + currentList, + ); + } + + if (attributes.blockquote) { + processBlockquoteLineBreak(mdast, currentParagraph); + return currentList; + } + + // Default case: regular paragraph + mdast.children.push(currentParagraph); + return null; +} + +/** + * Handles an empty line with special attributes. + * @param {MdastNode} mdast - The root MDAST node. + * @param {Object} attributes - The attributes for the line. + * @param {MdastNode|null} currentList - The current list being built. + * @returns {MdastNode|null} - The updated current list. + */ +function handleEmptyLineWithAttributes(mdast, attributes, currentList) { + if (attributes["code-block"]) { + mdast.children.push(createEmptyCodeBlock(attributes)); + return currentList; + } + + if (attributes.list) { + const list = ensureList(mdast, attributes, currentList); + list.children.push(createEmptyListItem()); + return list; + } + + if (attributes.blockquote) { + mdast.children.push(createEmptyBlockquote()); + return currentList; + } + + return null; +} + +/** + * Creates an empty code block MDAST node. + * @param {Object} attributes - The attributes for the code block. + * @returns {MdastNode} - The code block node. + */ +function createEmptyCodeBlock(attributes) { + return { + type: "code", + value: "", + lang: + attributes["code-block"] === "plain" ? null : attributes["code-block"], + }; +} + +/** + * Creates an empty list item MDAST node. + * @returns {MdastNode} - The list item node. + */ +function createEmptyListItem() { + return { + type: "listItem", + spread: false, + children: [{ type: "paragraph", children: [] }], + }; +} + +/** + * Creates an empty blockquote MDAST node. + * @returns {MdastNode} - The blockquote node. + */ +function createEmptyBlockquote() { + return { + type: "blockquote", + children: [{ type: "paragraph", children: [] }], + }; +} + +/** + * Processes a header line break. + * @param {MdastNode} mdast - The root MDAST node. + * @param {string} textBuffer - The text buffer for the current line. + * @param {Object} attributes - The attributes for the line. + * @returns {void} + */ +function processHeaderLineBreak(mdast, textBuffer, attributes) { + const lines = textBuffer.split("\n"); + + if (lines.length > 1) { + const lastLine = lines.pop(); + const previousLines = lines.join("\n"); + + if (previousLines) { + mdast.children.push({ + type: "paragraph", + children: [{ type: "text", value: previousLines }], + }); + } + + mdast.children.push({ + type: "heading", + depth: attributes.header, + children: [{ type: "text", value: lastLine }], + }); + } else { + mdast.children.push({ + type: "heading", + depth: attributes.header, + children: [{ type: "text", value: textBuffer }], + }); + } +} + +/** + * Processes a code block line break. + * @param {MdastNode} mdast - The root MDAST node. + * @param {string} textBuffer - The text buffer for the current line. + * @param {Object} attributes - The attributes for the line. + * @returns {void} + */ +function processCodeBlockLineBreak(mdast, textBuffer, attributes) { + const lang = + attributes["code-block"] === "plain" ? null : attributes["code-block"]; + + // Find the last code block with the same language + let lastCodeBlock = null; + for (let i = mdast.children.length - 1; i >= 0; i--) { + const child = mdast.children[i]; + if (child.type === "code" && child.lang === lang) { + lastCodeBlock = child; + break; + } + } + + if (lastCodeBlock) { + // Append to existing code block with same language + lastCodeBlock.value += `\n${textBuffer}`; + } else { + // Create new code block + mdast.children.push({ + type: "code", + value: textBuffer, + lang, + }); + } +} + +/** + * Ensures a list exists in the MDAST. + * @param {MdastNode} mdast - The root MDAST node. + * @param {Object} attributes - The attributes for the line. + * @param {MdastNode|null} currentList - The current list being built. + * @returns {MdastNode} - The list node. + */ +function ensureList(mdast, attributes, currentList) { + const isOrderedList = attributes.list === "ordered"; + + // If there's no current list or the list type doesn't match + if (!currentList || currentList.ordered !== isOrderedList) { + // Check if the last child is a list of the correct type + const lastChild = mdast.children[mdast.children.length - 1]; + if ( + lastChild && + lastChild.type === "list" && + lastChild.ordered === isOrderedList + ) { + // Use the last list if it matches the type + return lastChild; + } + + // Create a new list + const newList = { + type: "list", + ordered: isOrderedList, + spread: false, + children: [], + }; + mdast.children.push(newList); + return newList; + } + + return currentList; +} + +/** + * Processes a list line break. + * @param {MdastNode} mdast - The root MDAST node. + * @param {MdastNode} currentParagraph - The current paragraph being built. + * @param {Object} attributes - The attributes for the line. + * @param {MdastNode|null} currentList - The current list being built. + * @returns {MdastNode} - The updated list node. + */ +function processListLineBreak( + mdast, + currentParagraph, + attributes, + currentList, +) { + const list = ensureList(mdast, attributes, currentList); + + // Check if this list item already exists to avoid duplication + const paragraphContent = JSON.stringify(currentParagraph.children); + const isDuplicate = list.children.some( + (item) => + item.children?.length === 1 && + JSON.stringify(item.children[0].children) === paragraphContent, + ); + + if (!isDuplicate) { + const listItem = { + type: "listItem", + spread: false, + children: [currentParagraph], + }; + + list.children.push(listItem); + } + + return list; +} + +/** + * Processes a blockquote line break. + * @param {MdastNode} mdast - The root MDAST node. + * @param {MdastNode} currentParagraph - The current paragraph being built. + * @returns {void} + */ +function processBlockquoteLineBreak(mdast, currentParagraph) { + // Look for an existing blockquote with identical content to avoid duplication + const paragraphContent = JSON.stringify(currentParagraph.children); + const existingBlockquote = mdast.children.find( + (child) => + child.type === "blockquote" && + child.children?.length === 1 && + JSON.stringify(child.children[0].children) === paragraphContent, + ); + + if (!existingBlockquote) { + mdast.children.push({ + type: "blockquote", + children: [currentParagraph], + }); + } +} + +// Main execution +document.addEventListener("DOMContentLoaded", initializeEditors); diff --git a/examples/rich-text-editor/sqlpage/migrations/01_blog_posts.sql b/examples/rich-text-editor/sqlpage/migrations/01_blog_posts.sql new file mode 100644 index 00000000..c81195f6 --- /dev/null +++ b/examples/rich-text-editor/sqlpage/migrations/01_blog_posts.sql @@ -0,0 +1,5 @@ +create table blog_posts ( + id integer primary key autoincrement, + title text not null, + content text not null +); \ No newline at end of file diff --git a/examples/roundest_pokemon_rating/Dockerfile b/examples/roundest_pokemon_rating/Dockerfile new file mode 100644 index 00000000..3e67314a --- /dev/null +++ b/examples/roundest_pokemon_rating/Dockerfile @@ -0,0 +1,5 @@ +FROM lovasoa/sqlpage:latest + +COPY sqlpage /etc/sqlpage +COPY src /var/www +ENV DATABASE_URL=sqlite:///tmp/pokemon.db?mode=rwc diff --git a/examples/roundest_pokemon_rating/README.md b/examples/roundest_pokemon_rating/README.md new file mode 100644 index 00000000..e9e44a20 --- /dev/null +++ b/examples/roundest_pokemon_rating/README.md @@ -0,0 +1,27 @@ +# Roundest (SQLPage Version) + +This is a simple web app that allows you to vote on which Pokemon is the most round. + +| Vote UI | Results UI | +| --- | --- | +| ![vote ui screenshot](screenshots/vote.png) | ![results ui](screenshots/results.png) | + +It demonstrates how to build an entirely custom web app, +without using any of the pre-built components of SQLPage. + +All the custom components are in the [`sqlpage/templates/`](./sqlpage/templates/) folder. + +## Running the app + +### Using an installed version of SQLPage + +``` +sqlpage --web-root src +``` + +### Using Docker + +``` +docker build -t roundest-sqlpage . +docker run -p 8080:8080 -it roundest-sqlpage +``` diff --git a/examples/roundest_pokemon_rating/screenshots/results.png b/examples/roundest_pokemon_rating/screenshots/results.png new file mode 100644 index 00000000..03d23c13 Binary files /dev/null and b/examples/roundest_pokemon_rating/screenshots/results.png differ diff --git a/examples/roundest_pokemon_rating/screenshots/vote.png b/examples/roundest_pokemon_rating/screenshots/vote.png new file mode 100644 index 00000000..4f55223b Binary files /dev/null and b/examples/roundest_pokemon_rating/screenshots/vote.png differ diff --git a/examples/roundest_pokemon_rating/sqlpage/migrations/0000_pokemon_table.sql b/examples/roundest_pokemon_rating/sqlpage/migrations/0000_pokemon_table.sql new file mode 100644 index 00000000..6c03f699 --- /dev/null +++ b/examples/roundest_pokemon_rating/sqlpage/migrations/0000_pokemon_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS pokemon ( + dex_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + up_votes INTEGER DEFAULT 0, + down_votes INTEGER DEFAULT 0 +); \ No newline at end of file diff --git a/examples/roundest_pokemon_rating/sqlpage/templates/pokemon.handlebars b/examples/roundest_pokemon_rating/sqlpage/templates/pokemon.handlebars new file mode 100644 index 00000000..8aa1fe84 --- /dev/null +++ b/examples/roundest_pokemon_rating/sqlpage/templates/pokemon.handlebars @@ -0,0 +1,30 @@ +
+
+ {{#each_row}} +
+ + {{name}} +
+ #{{dexNumber}} +

{{name}}

+
+ +
+
+
+ {{/each_row}} +
+
\ No newline at end of file diff --git a/examples/roundest_pokemon_rating/sqlpage/templates/results.handlebars b/examples/roundest_pokemon_rating/sqlpage/templates/results.handlebars new file mode 100644 index 00000000..895ea772 --- /dev/null +++ b/examples/roundest_pokemon_rating/sqlpage/templates/results.handlebars @@ -0,0 +1,29 @@ +
+
+ {{#each_row}} +
+
+
+ #{{rank}} +
+ + {{name}} + +
+
#{{dex_id}}
+

{{name}}

+
+ +
+
+ {{win_percentage}}% +
+
+ {{up_votes}}W - {{down_votes}}L +
+
+
+
+ {{/each_row}} +
+
diff --git a/examples/roundest_pokemon_rating/sqlpage/templates/shell.handlebars b/examples/roundest_pokemon_rating/sqlpage/templates/shell.handlebars new file mode 100644 index 00000000..69e61e80 --- /dev/null +++ b/examples/roundest_pokemon_rating/sqlpage/templates/shell.handlebars @@ -0,0 +1,39 @@ + + + + + +
+ +
+ +
{{~#each_row~}}{{~/each_row~}}
+ + + + \ No newline at end of file diff --git a/examples/roundest_pokemon_rating/src/favicon.ico b/examples/roundest_pokemon_rating/src/favicon.ico new file mode 100644 index 00000000..c7a8e9ab Binary files /dev/null and b/examples/roundest_pokemon_rating/src/favicon.ico differ diff --git a/examples/roundest_pokemon_rating/src/index.sql b/examples/roundest_pokemon_rating/src/index.sql new file mode 100644 index 00000000..cba0da79 --- /dev/null +++ b/examples/roundest_pokemon_rating/src/index.sql @@ -0,0 +1,8 @@ +select 'redirect' as component, '/populate.sql' as link +where not exists(select 1 from pokemon); + +select 'pokemon' as component; + +select dex_id as dexNumber, name +from pokemon +order by random() limit 2; diff --git a/examples/roundest_pokemon_rating/src/populate.sql b/examples/roundest_pokemon_rating/src/populate.sql new file mode 100644 index 00000000..039802ec --- /dev/null +++ b/examples/roundest_pokemon_rating/src/populate.sql @@ -0,0 +1,12 @@ +-- This updates our pokemon table with fresh data from pokeapi.co +insert into pokemon (dex_id, name) +select + cast(rtrim(substr(value->>'url', + length('https://pokeapi.co/api/v2/pokemon/') + 1), + '/') as integer) as dex_id, + value->>'name' as name +from json_each( + sqlpage.fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0') -> 'results' +); + +select 'redirect' as component, '/' as link; \ No newline at end of file diff --git a/examples/roundest_pokemon_rating/src/results.sql b/examples/roundest_pokemon_rating/src/results.sql new file mode 100644 index 00000000..676f7193 --- /dev/null +++ b/examples/roundest_pokemon_rating/src/results.sql @@ -0,0 +1,15 @@ +select 'results' as component; + +with ranked as ( + select + *, + case + when (up_votes + down_votes) = 0 then 0 + else 100 * up_votes / (up_votes + down_votes) + end as win_percentage, + up_votes - down_votes as score + from pokemon +) +select *, rank() over (order by score desc) as rank +from ranked +order by win_percentage desc, score desc; diff --git a/examples/roundest_pokemon_rating/src/vote.sql b/examples/roundest_pokemon_rating/src/vote.sql new file mode 100644 index 00000000..3929517e --- /dev/null +++ b/examples/roundest_pokemon_rating/src/vote.sql @@ -0,0 +1,7 @@ +update pokemon +set + up_votes = up_votes + (dex_id = $voted), + down_votes = down_votes + (dex_id != $voted) +where dex_id IN (:option_0, :option_1); + +select 'redirect' as component, '/' as link; diff --git a/examples/sending emails/README.md b/examples/sending emails/README.md new file mode 100644 index 00000000..e45b1b6e --- /dev/null +++ b/examples/sending emails/README.md @@ -0,0 +1,76 @@ +# Sending Emails with SQLPage + +SQLPage lets you interact with any email service through their API, +using the [`sqlpage.fetch` function](https://sql-page.com/functions.sql?function=fetch). + +## Why Use an Email Service? + +Sending emails directly from your server can be challenging: +- Many ISPs block direct email sending to prevent spam +- Email deliverability requires proper setup of SPF, DKIM, and DMARC records +- Managing bounce handling and spam complaints is complex +- Direct sending can impact your server's IP reputation + +Email services solve these problems by providing reliable APIs for sending emails while handling deliverability, tracking, and compliance. + +## Popular Email Services + +- [Mailgun](https://www.mailgun.com/) - Developer-friendly, great for transactional emails +- [SendGrid](https://sendgrid.com/) - Powerful features, owned by Twilio +- [Amazon SES](https://aws.amazon.com/ses/) - Cost-effective for high volume +- [Postmark](https://postmarkapp.com/) - Focused on transactional email delivery +- [SMTP2GO](https://www.smtp2go.com/) - Simple SMTP service with API options + +## Example: Sending Emails with Mailgun + +Here's a complete example using Mailgun's API to send emails through SQLPage: + +### [`email.sql`](./email.sql) +```sql +-- Configure the email request +set email_request = json_object( + 'url', 'https://api.mailgun.net/v3/' || sqlpage.environment_variable('MAILGUN_DOMAIN') || '/messages', + 'method', 'POST', + 'headers', json_object( + 'Content-Type', 'application/x-www-form-urlencoded', + 'Authorization', 'Basic ' || encode(('api:' || sqlpage.environment_variable('MAILGUN_API_KEY'))::bytea, 'base64') + ), + 'body', + 'from=Your Name ' + || '&to=' || $to_email + || '&subject=' || $subject + || '&text=' || $message_text + || '&html=' || $message_html +); + +-- Send the email using sqlpage.fetch +set email_response = sqlpage.fetch($email_request); + +-- Handle the response +select + 'alert' as component, + case + when $email_response->>'id' is not null then 'Email sent successfully' + else 'Failed to send email: ' || ($email_response->>'message') + end as title; +``` + +### Setup Instructions + +1. Sign up for a [Mailgun account](https://signup.mailgun.com/new/signup) +2. Verify your domain or use the sandbox domain for testing +3. Get your API key from the Mailgun dashboard +4. Set these environment variables in your SQLPage configuration: + ``` + MAILGUN_API_KEY=your-api-key-here + MAILGUN_DOMAIN=your-domain.com + ``` + +## Best Practices + +- If you share your code with others, it should not contain sensitive data like API keys + - Instead, use environment variables with [`sqlpage.environment_variable`](https://sql-page.com/functions.sql?function=environment_variable) +- Implement proper error handling +- Consider rate limiting for bulk sending +- Include unsubscribe links when sending marketing emails +- Follow email regulations (GDPR, CAN-SPAM Act) diff --git a/examples/sending emails/email.sql b/examples/sending emails/email.sql new file mode 100644 index 00000000..c528cc13 --- /dev/null +++ b/examples/sending emails/email.sql @@ -0,0 +1,43 @@ +-- Configure the email request + +-- Obtain the authorization by encoding "api:YOUR_PERSONAL_API_KEY" in base64 +set authorization = 'YXBpOjI4ODlmODE3Njk5ZjZiNzA4MTdhODliOGUwODYyNmEyLWU2MWFlOGRkLTgzMjRjYWZm'; + +-- Find the domain in your Mailgun account +set domain = 'sandbox859545b401674a95b906ab417d48c97c.mailgun.org'; + +-- Set the recipient email address. +--In this demo, we accept sending any email to any address. +-- If you do this in production, spammers WILL use your account to send spam. +-- Your application should only allow emails to be sent to addresses you have verified. +set to_email = :to_email; + +-- Set the email subject +set subject = :subject; + +-- Set the email message text +set message_text = :message_text; + +set email_request = json_object( + 'url', 'https://api.mailgun.net/v3/' || $domain || '/messages', + 'method', 'POST', + 'headers', json_object( + 'Content-Type', 'application/x-www-form-urlencoded', + 'Authorization', 'Basic ' || $authorization + ), + 'body', + 'from=Your Name ' + || '&to=' || sqlpage.url_encode($to_email) + || '&subject=' || sqlpage.url_encode($subject) + || '&text=' || sqlpage.url_encode($message_text) +); +-- Send the email using sqlpage.fetch +set email_response = sqlpage.fetch($email_request); + +-- Handle the response +select + 'alert' as component, + case + when $email_response->>'id' is not null then 'Email sent successfully' + else 'Failed to send email: ' || ($email_response->>'message') + end as title; \ No newline at end of file diff --git a/examples/sending emails/index.sql b/examples/sending emails/index.sql new file mode 100644 index 00000000..dcee9813 --- /dev/null +++ b/examples/sending emails/index.sql @@ -0,0 +1,5 @@ +select 'form' as component, 'Send an email' as title, 'email.sql' as action; + +select 'to_email' as name, 'To email' as label, 'recipient@example.com' as value; +select 'subject' as name, 'Subject' as label, 'Test email' as value; +select 'textarea' as type, 'message_text' as name, 'Message' as label, 'This is a test email' as value; diff --git a/examples/simple-website-example/README.md b/examples/simple-website-example/README.md index 4bbb93a9..df8d5dd3 100644 --- a/examples/simple-website-example/README.md +++ b/examples/simple-website-example/README.md @@ -5,8 +5,8 @@ This is a very simple example of a website that uses the SQLPage web application This website illustrates how to create a basic Create-Read-Update-Delete (CRUD) application using SQLPage. It has the following bsic features: - - Displays a list of user names using the [list component](https://sql.ophir.dev/documentation.sql?component=list#component) (in [`index.sql`](./index.sql#L14-L20)) - - Add a new user name to the list through a [form](https://sql.ophir.dev/documentation.sql?component=form#component) (in [`index.sql`](./index.sql#L1-L9)) + - Displays a list of user names using the [list component](https://sql-page.com/documentation.sql?component=list#component) (in [`index.sql`](./index.sql#L14-L20)) + - Add a new user name to the list through a [form](https://sql-page.com/documentation.sql?component=form#component) (in [`index.sql`](./index.sql#L1-L9)) - View a user's personal page by clicking on a name in the list (in [`user.sql`](./user.sql)) - Delete a user from the list by clicking on the delete button in the user's personal page (in [`delete.sql`](./delete.sql)) diff --git a/examples/simple-website-example/edit.sql b/examples/simple-website-example/edit.sql index 51380c5e..4434a18a 100644 --- a/examples/simple-website-example/edit.sql +++ b/examples/simple-website-example/edit.sql @@ -1,6 +1,8 @@ -select 'form' as component; -select 'text' as type, 'Username' as name, username as value -from users where id = $id; +update users +set username = :Username, + is_admin = :Administrator is not null +where :Username is not null and id = $id; -update users set username = :Username -where id = $id and :Username is not null; \ No newline at end of file +select 'form' as component; +select 'text' as type, 'Username' as name, username as value from users where id = $id; +select 'checkbox' as type, 'Has administrator privileges' as label, 'Administrator' as name, is_admin as checked from users where id = $id; \ No newline at end of file diff --git a/examples/simple-website-example/index.sql b/examples/simple-website-example/index.sql index 5ef43210..8ff4bed8 100644 --- a/examples/simple-website-example/index.sql +++ b/examples/simple-website-example/index.sql @@ -15,6 +15,7 @@ SELECT 'list' AS component, 'Users' AS title; SELECT username AS title, username || ' is a user on this website.' as description, + case when is_admin then 'red' end as color, 'user' as icon, 'user.sql?id=' || id as link FROM users; \ No newline at end of file diff --git a/examples/simple-website-example/sqlpage/migrations/0001_create_users_table.sql b/examples/simple-website-example/sqlpage/migrations/0001_create_users_table.sql index 1e1edef9..51e0d22f 100644 --- a/examples/simple-website-example/sqlpage/migrations/0001_create_users_table.sql +++ b/examples/simple-website-example/sqlpage/migrations/0001_create_users_table.sql @@ -1,4 +1,5 @@ CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL + username TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/examples/simple-website-example/sqlpage/sqlpage.json b/examples/simple-website-example/sqlpage/sqlpage.json index 78995eb2..268bd02b 100644 --- a/examples/simple-website-example/sqlpage/sqlpage.json +++ b/examples/simple-website-example/sqlpage/sqlpage.json @@ -1,3 +1,3 @@ { - "database_url": "sqlite://:memory:" -} \ No newline at end of file + "database_url": "sqlite://:memory:" +} diff --git a/examples/single sign on/README.md b/examples/single sign on/README.md index ad89a5aa..c947d363 100644 --- a/examples/single sign on/README.md +++ b/examples/single sign on/README.md @@ -1,7 +1,7 @@ # SQLPage Single Sign-On demo This project demonstrates how to implement -external authentication (Single Sign-On) in a SQLPage application. +external authentication (Single Sign-On) in a SQLPage application using SQLPage's built-in OIDC support. It demonstrates the implementation of two external authentication protocols: - [OpenID Connect (OIDC)](https://openid.net/connect/) @@ -20,7 +20,7 @@ Depending on your use case, you can choose the appropriate protocol for your app To run the demo, you just need docker and docker-compose installed on your machine. Then, run the following commands: ```bash -docker-compose up +docker compose up --watch ``` This will start a Keycloak server and a SQLPage server. You can access the SQLPage application at http://localhost:8080. @@ -42,66 +42,76 @@ the [CAS protocol](https://apereo.github.io/cas/) (version 3.0), which is mostly OIDC is an authentication protocol that allows users to authenticate with a third-party identity provider and then access applications without having to log in again. This is useful for single sign-on (SSO) scenarios where users need to access multiple applications with a single set of credentials. OIDC can be used to implement a "Login with Google" or "Login with Facebook" button in your application, since these providers support the OIDC protocol. -SQLPage currently doesn't have a native OIDC implementation, but you can implement OIDC authentication in your SQLPage app yourself. +SQLPage has built-in support for OIDC authentication since v0.35. +This project demonstrates how to use it with the free and open source [Keycloak](https://www.keycloak.org/) OIDC provider. +You can easily replace Keycloak with another OIDC provider, such as Google, or your enterprise OIDC provider, by following the steps in the [Configuration](#configuration) section. -This project provides a basic implementation of OIDC authentication in a SQLPage application. It uses the free and open source [Keycloak](https://www.keycloak.org/) OIDC provider -to authenticate users. You can easily replace Keycloak with another OIDC provider, such as Google, or your enterprise OIDC provider, by following the steps in the [Configuration](#configuration) section. +### Public and Protected Pages -### Configuration +By default, SQLPage's built-in OIDC support protects the entire website. However, you can configure it to have a mix of public and protected pages using the `oidc_protected_paths` option in your `sqlpage.json` file. -If you want to use this implementation in your own SQLPage application, -with a different OIDC provider, here are the steps you need to follow: +This allows you to create a public-facing area (like a homepage with a login button) and a separate protected area for authenticated users. -1. Create an OIDC application in your OIDC provider (e.g., Keycloak). You will need to provide the following information: - - **Client type** (`public` or `confidential`). For this implementation, you should use `confidential` (sometimes called `regular web application:`, `server-side`, `backend`, or `Authorization Code Flow`). In Keycloak, this is set by switching on the `Client Authentication` toggle. - - **Client ID**: This is a unique identifier for your application. Choose a short and descriptive name for your application without spaces or special characters. - - **Redirect URI**: This is the URL of your SQLPage application, followed by `/oidc_redirect_handler.sql`. For example, `https://example.com/oidc_redirect_handler.sql`. - - **Logout redirect URI**: This is the URL where the user should be redirected after logging out. For this implementation, we use the home page URL: `https://example.com/`. -2. Once the application is created, the provider will give you the following information: - - **Client secret**: This is a secret key that is used to authenticate your application with the OIDC provider. You will need to provide this value to your SQLPage application as an environment variable. +### Configuration +To use OIDC authentication in your own SQLPage application, +you need to configure it in your `sqlpage.json` file: + +```json +{ + "oidc_issuer_url": "https://your-keycloak-server/auth/realms/your-realm", + "oidc_client_id": "your-client-id", + "oidc_client_secret": "your-client-secret", + "host": "localhost:8080", + "oidc_protected_paths": ["/protected"] +} +``` -3. Once you have the client ID and client secret, you can configure your SQLPage application to use OIDC authentication. You will need to set the following [environment variables](https://en.wikipedia.org/wiki/Environment_variable) in your SQLPage application: +The configuration parameters are: +- `oidc_issuer_url`: The base URL of your OIDC provider +- `oidc_client_id`: The ID that identifies your SQLPage application to the OIDC provider +- `oidc_client_secret`: The secret key for your SQLPage application +- `host`: The web address where your application is accessible -- `OIDC_CLIENT_ID`: The value you chose for the client ID of your OIDC application. -- `OIDC_CLIENT_SECRET`: The client secret of your OIDC application that you received from the OIDC provider in step 2. -- `OIDC_AUTHORIZATION_ENDPOINT`: The authorization endpoint of your OIDC provider. This is the URL where the user is redirected to log in. For Keycloak, this is usually `your-keycloak-url/auth/realms/master/protocol/openid-connect/auth`. For Google, this is `https://accounts.google.com/o/oauth2/auth`. -- `OIDC_TOKEN_ENDPOINT`: The token endpoint of your OIDC provider. This is the URL where the application exchanges the authorization code for an access token. For Keycloak, this is usually `your-keycloak-url/auth/realms/master/protocol/openid-connect/token`. For Google, this is `https://oauth2.googleapis.com/token`. -- `OIDC_USERINFO_ENDPOINT`: The userinfo endpoint of your OIDC provider. This is the URL where the application can retrieve information about the authenticated user. For Keycloak, this is usually `your-keycloak-url/auth/realms/master/protocol/openid-connect/userinfo`. For Google, this is `https://openidconnect.googleapis.com/v1/userinfo`. -- `OIDC_END_SESSION_ENDPOINT`: The logout endpoint of your OIDC provider. This is the URL where the application can redirect the user to log out. For Keycloak, this is usually `your-keycloak-url/auth/realms/master/protocol/openid-connect/logout`. +### Accessing User Information -In order to find the various endpoints for your OIDC provider, you can refer to the OIDC provider's **Discovery Document**, at the URL `base-url/.well-known/openid-configuration`. +Once OIDC is configured, you can access information about the authenticated user in your SQL files using these functions: -Here is a screenshot of the Keycloak configuration for the demo application: +- `sqlpage.user_info(claim_name)`: Get a specific claim about the user (like name or email) +- `sqlpage.user_info_token()`: Get the entire identity token as JSON -![Keycloak Configuration](assets/keycloak_configuration.png) +Example: +```sql +select 'text' as component, 'Welcome, ' || sqlpage.user_info('name') || '!' as contents_md; +``` -## Code Overview +### Implementation Details -### `login.sql` +The demo includes several SQL files that demonstrate different aspects of OIDC integration: -The [`login.sql`](./login.sql) file simply redirects the user to the OIDC provider's authorization endpoint. -The provider is then responsible for authenticating the user and redirecting them back to the SQLPage application's `oidc_redirect_handler.sql` script. +1. `index.sql`: A public page that shows a welcome message and a login button. If the user is logged in, it displays their email and a link to the protected page. -### `oidc_redirect_handler.sql` -The main logic is contained in the [`oidc_redirect_handler.sql`](./oidc_redirect_handler.sql) -file. This script handles the OIDC redirect after the user has authenticated with the OIDC provider. It performs the following steps: +2. `protected.sql`: A page that is only accessible to authenticated users. It displays the user's information. -1. Checks if the `oauth_state` cookie matches the `state` parameter in the query string. This is a security measure to prevent CSRF attacks. If the states do not match, the user is redirected to the login page. +3. `logout.sql`: Logs the user out by removing the authentication cookie and redirecting to the OIDC provider's logout page. -2. Exchanges the authorization code for an access token. This is done by making a POST request to the OIDC provider's token endpoint. The request includes the authorization code, the redirect URI, and the client ID and secret. +### Docker Setup -3. If the access token cannot be obtained, the user is redirected to the login page. +The demo uses Docker Compose to set up both SQLPage and Keycloak. The configuration includes: -### `logout.sql` +- SQLPage service with: + - Volume mounts for the web root and configuration + - CAS configuration for optional CAS support + - Debug logging enabled -The [`logout.sql`](./logout.sql) file simply clears the `session_id` cookie, -removes the session information from the database, and redirects the user to the OIDC provider's logout endpoint. +- Keycloak service with: + - Pre-configured realm and users + - Health checks to ensure it's ready before SQLPage starts + - Admin credentials for management ## References -- An accessible explanation of OIDC: https://annotate.dev/p/hello-world/learn-oauth-2-0-by-building-your-own-oauth-client-U2HaZNtvQojn4F +- [SQLPage OIDC Documentation](https://sql-page.com/sso) - [OpenID Connect](https://openid.net/connect/) - [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) - diff --git a/examples/single sign on/cas/index.sql b/examples/single sign on/cas/index.sql index 0db0b8b1..6963abf2 100644 --- a/examples/single sign on/cas/index.sql +++ b/examples/single sign on/cas/index.sql @@ -1,4 +1,4 @@ -set $user_email = (select email from user_sessions where session_id = sqlpage.cookie('session_id')); +set user_email = (select email from user_sessions where session_id = sqlpage.cookie('session_id')); select 'text' as component, 'You are not authenticated. [Log in](login.sql).' as contents_md where $user_email is null; select 'text' as component, 'Welcome, ' || $user_email || '. You can now [log out](logout.sql).' as contents_md where $user_email is not null; diff --git a/examples/single sign on/cas/redirect_handler.sql b/examples/single sign on/cas/redirect_handler.sql index 70af5fb3..6c98efe3 100644 --- a/examples/single sign on/cas/redirect_handler.sql +++ b/examples/single sign on/cas/redirect_handler.sql @@ -7,7 +7,7 @@ select 'redirect' as component, '/cas/' as link where $ticket is null; -- We must then validate the ticket with the CAS server -- CAS v3 specifies the following URL for ticket validation (see https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#28-p3servicevalidate-cas-30) -- https://cas.example.org/p3/serviceValidate?ticket=ST-1856339-aA5Yuvrxzpv8Tau1cYQ7&service=http://myclient.example.org/myapp&format=JSON -SET $ticket_url = +set ticket_url = sqlpage.environment_variable('CAS_ROOT_URL') || '/p3/serviceValidate' || '?ticket=' || sqlpage.url_encode($ticket) @@ -15,7 +15,7 @@ SET $ticket_url = || '&format=JSON'; -- We must then make a request to the CAS server to validate the ticket -set $validation_response = sqlpage.fetch($ticket_url); +set validation_response = sqlpage.fetch($ticket_url); -- If the ticket is invalid, the CAS server will return a 200 OK response with a JSON object like this: -- { "serviceResponse": { "authenticationFailure": { "code": "INVALID_TICKET", "description": "..." } } } diff --git a/examples/single sign on/docker-compose.yaml b/examples/single sign on/docker-compose.yaml index 9aa93a8c..c54f9507 100644 --- a/examples/single sign on/docker-compose.yaml +++ b/examples/single sign on/docker-compose.yaml @@ -9,18 +9,12 @@ services: sqlpage: image: lovasoa/sqlpage:main # Use the latest development version of SQLPage + build: + context: ../.. volumes: - .:/var/www - ./sqlpage:/etc/sqlpage environment: - # OIDC configuration - - OIDC_AUTHORIZATION_ENDPOINT=http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/auth - - OIDC_TOKEN_ENDPOINT=http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/token - - OIDC_USERINFO_ENDPOINT=http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/userinfo - - OIDC_END_SESSION_ENDPOINT=http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/logout - - OIDC_CLIENT_ID=sqlpage - - OIDC_CLIENT_SECRET=qiawfnYrYzsmoaOZT28rRjPPRamfvrYr - # CAS (central authentication system) configuration # (you can ignore this if you're only using OpenID Connect) - CAS_ROOT_URL=http://localhost:8181/realms/sqlpage_demo/protocol/cas @@ -28,6 +22,13 @@ services: # SQLPage configuration - RUST_LOG=sqlpage=debug network_mode: host + depends_on: + keycloak: + condition: service_healthy + develop: + watch: + - action: restart + path: ./sqlpage/ keycloak: build: @@ -39,3 +40,9 @@ services: volumes: - ./keycloak-configuration.json:/opt/keycloak/data/import/realm.json network_mode: host + healthcheck: + test: ["CMD-SHELL", "/opt/keycloak/bin/kcadm.sh get realms/sqlpage_demo --server http://localhost:8181 --realm master --user admin --password admin || exit 1"] + interval: 10s + timeout: 2s + retries: 5 + start_period: 5s diff --git a/examples/single sign on/index.sql b/examples/single sign on/index.sql index 7645020b..bc410f69 100644 --- a/examples/single sign on/index.sql +++ b/examples/single sign on/index.sql @@ -1,15 +1,27 @@ -set $user_email = (select email from user_sessions where session_id = sqlpage.cookie('session_id')); +SELECT 'shell' as component, 'My public app' as title; -select 'shell' as component, 'My secure app' as title, - (case when $user_email is null then 'login' else 'logout' end) as menu_item; +set email = sqlpage.user_info('email'); -select 'text' as component, sqlpage.read_file_as_text('assets/homepage.md') as contents_md where $user_email is null; +-- For anonymous users +SELECT 'hero' as component, + '/protected' as link, + 'Log in' as link_text, + 'Welcome' as title, + 'You are currently browsing as a guest. Log in to access the protected page.' as description, + '/protected/public/hello.jpeg' as image +WHERE $email IS NULL; -select 'text' as component, - 'You''re in !' as title, - 'You are now logged in as *`' || $user_email || '`*. -You have access to the [protected page](protected.sql). - -![open door](/assets/welcome.jpeg)' - as contents_md -where $user_email is not null; \ No newline at end of file +-- For logged-in users +SELECT 'text' as component, + 'Welcome back, ' || sqlpage.user_info('name') || '!' as title, + 'You are logged in as ' || sqlpage.user_info('email') || + '. You can now access the [protected page](/protected) or [log out](' || + -- Secure OIDC logout with CSRF protection + -- This redirects to /sqlpage/oidc_logout which: + -- 1. Verifies the CSRF token + -- 2. Removes the auth cookies + -- 3. Redirects to the OIDC provider's logout endpoint + -- 4. Finally redirects back to the homepage + sqlpage.oidc_logout_url() + || ').' as contents_md +WHERE $email IS NOT NULL; diff --git a/examples/single sign on/keycloak-configuration.json b/examples/single sign on/keycloak-configuration.json index daf0b9d0..f0f25017 100644 --- a/examples/single sign on/keycloak-configuration.json +++ b/examples/single sign on/keycloak-configuration.json @@ -1,4402 +1,5195 @@ -[ { - "id" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "realm" : "master", - "displayName" : "Keycloak", - "displayNameHtml" : "
Keycloak
", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 60, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : true, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxTemporaryLockouts" : 0, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "fa37048d-c55d-487b-9f05-5b2ecd1ad183", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "attributes" : { } - }, { - "id" : "4c2fe5cf-a97e-4f1f-9f6c-cc60e37f1716", - "name" : "create-realm", - "description" : "${role_create-realm}", - "composite" : false, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "attributes" : { } - }, { - "id" : "e0046137-2806-4965-88e0-0c91ae41b926", - "name" : "admin", - "description" : "${role_admin}", - "composite" : true, - "composites" : { - "realm" : [ "create-realm" ], - "client" : { - "sqlpage_demo-realm" : [ "query-groups", "manage-identity-providers", "view-identity-providers", "create-client", "manage-events", "manage-users", "view-clients", "manage-realm", "view-events", "query-realms", "manage-clients", "manage-authorization", "view-users", "view-authorization", "view-realm", "impersonation", "query-users", "query-clients" ], - "master-realm" : [ "view-clients", "create-client", "query-realms", "manage-identity-providers", "query-users", "manage-clients", "view-users", "manage-events", "view-realm", "query-clients", "manage-users", "view-events", "query-groups", "impersonation", "manage-authorization", "manage-realm", "view-authorization", "view-identity-providers" ] +[ + { + "id": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "realm": "master", + "displayName": "Keycloak", + "displayNameHtml": "
Keycloak
", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 60, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "157f6b82-51a3-41ec-ace0-84301dcd9c25", + "name": "admin", + "description": "${role_admin}", + "composite": true, + "composites": { + "realm": ["create-realm"], + "client": { + "sqlpage_demo-realm": [ + "view-realm", + "impersonation", + "query-clients", + "manage-identity-providers", + "manage-users", + "query-realms", + "create-client", + "view-authorization", + "manage-realm", + "manage-authorization", + "manage-events", + "view-events", + "view-clients", + "view-identity-providers", + "view-users", + "query-groups", + "manage-clients", + "query-users" + ], + "master-realm": [ + "query-users", + "manage-realm", + "manage-clients", + "manage-authorization", + "impersonation", + "manage-users", + "query-groups", + "view-authorization", + "view-identity-providers", + "query-clients", + "view-realm", + "manage-identity-providers", + "view-clients", + "create-client", + "view-events", + "query-realms", + "view-users", + "manage-events" + ] + } + }, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "attributes": {} + }, + { + "id": "8f554927-b25f-488a-9c9f-f57a0a543dc7", + "name": "create-realm", + "description": "${role_create-realm}", + "composite": false, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "attributes": {} + }, + { + "id": "7a9a5437-5ed4-4c07-93c2-312bab85725c", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": ["offline_access", "uma_authorization"], + "client": { + "account": ["manage-account", "view-profile"] + } + }, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "attributes": {} + }, + { + "id": "3b4b330e-9ef3-4226-bd0f-dd2537083a5d", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "attributes": {} + }, + { + "id": "d4ead458-2552-44b9-9ab2-7c053e41f44e", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855", + "attributes": {} } + ], + "client": { + "sqlpage_demo-realm": [ + { + "id": "8fb6a790-d944-4ad4-949b-40033e8237be", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "ed155122-f596-437a-bf96-d7233b5a2a25", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "ca72acd4-8e77-4f50-91dc-f8a80dcb4be2", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "957f208f-f775-4c56-bbaa-6d620ae03000", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "b17b9e52-b250-45ae-abff-c8f1243507ef", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "711beb97-dd68-4bc8-8bd0-3a545849d1f6", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "4b25dd76-f5a8-47f6-be6c-50810b8159b1", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "bcd0f88e-5810-41ad-b8b3-84daf2578b59", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "8754e583-158e-45cc-9a2d-ed14c546aae6", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "sqlpage_demo-realm": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "bc742aa2-d368-4bbd-9b3d-de521d6c0c74", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "7d290769-fb64-4609-a7ac-faabd85da902", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "38b8ad0e-c36e-491e-bba5-a2b66e407988", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "a1d78e33-27a4-4a14-a6d5-bb5cc670647f", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "08fbd2d8-4e7d-4989-b3cf-6b3be44f53c2", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "sqlpage_demo-realm": ["query-groups", "query-users"] + } + }, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "8c4fa339-791d-40de-8add-2de668260a43", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "63bd8ea4-2e0c-4bd5-a119-40cfc6bee1cc", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "67f702c1-e7a5-41e9-bffc-0789969518a8", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + }, + { + "id": "aced8cae-7161-4d5b-bf26-d2a7ab7b14b1", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "560a2c01-0691-4698-a367-0b489f12b11d", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "82980459-069c-4bf9-99db-a521b6c4f0bd", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "a371454e-3a21-45fb-b634-7b9ee4bb9647", + "attributes": {} + } + ], + "master-realm": [ + { + "id": "f87d166f-1b7b-483c-afee-35a6fe49a34d", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "24c469bd-7cb7-4c8a-b8b7-54aeb2d3c5b8", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "cf002990-c6f8-43f0-a897-adb26881caa7", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "bd94e0cc-2ff3-487a-8076-5f4d65dd98d8", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "38430b67-2ba0-414f-af61-b07d4cc4704d", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "f44e09f2-0191-4edb-bcb7-6ef8cef5ef91", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "8ea944a9-0c18-4e6c-9da6-f610fb22aa2e", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "f4c97dbe-f4ae-4ff3-914a-dda48eb165fd", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "fa43ce74-999c-4d7a-bf95-49462982683c", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "0a810999-e124-4202-ab79-4dd835ca4f33", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "e3bb431d-3b63-4b6f-a76b-3ad95bcafc4c", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "master-realm": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "03a13c0b-4fe5-48cd-aa70-6802ad07e4ab", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "38929e9c-19a7-4ab4-ad6b-2089a137c8e4", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "3118f7c6-c071-4aaf-8c63-0a6f05c8c633", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "1b745171-139b-448e-b395-c5142178c359", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "0371bdc2-bca8-414b-bef8-f730bb83cf9d", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "master-realm": ["query-users", "query-groups"] + } + }, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "92ce5daa-5ba8-4bde-a612-da0c027d621c", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + }, + { + "id": "822ad55f-8dd9-4300-a7b9-9c0f03ecadf9", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "1ee50014-2a1c-4061-a55e-828b723858b5", + "attributes": {} + } + ], + "account": [ + { + "id": "76fa7fbc-9f5f-45aa-96c0-200cdc49b8e1", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": ["manage-account-links"] + } + }, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "931743f6-48c6-4099-945d-c54acbfa0728", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": ["view-consent"] + } + }, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "584d55e9-c784-4dc2-bb6d-f9e4ce2b98fd", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "d0b8cafa-537c-46eb-8a9d-d9eb3ea92260", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "f4bf658d-b930-4f7a-b77d-4fff3752c38b", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "08a925a0-281b-41eb-8c59-324b71b90fe0", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "a0943dbf-5b50-48b2-96d3-53cf5d884d93", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + }, + { + "id": "d1c3b3ba-aa46-4e63-a8ea-457b354a720b", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "7a9a5437-5ed4-4c07-93c2-312bab85725c", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "35cfdc9d-86a8-4974-a40c-adf25d4c9855" + }, + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "04a670c1-5e58-4856-813b-4e228409ab05", + "username": "admin", + "emailVerified": false, + "createdTimestamp": 1753803522059, + "enabled": true, + "totp": false, + "credentials": [ + { + "id": "77364ac0-e861-4458-b4af-01565afaea76", + "type": "password", + "createdDate": 1753803522306, + "secretData": "{\"value\":\"JceAANUOmsxhmF6x9wVh7sEzNY4+hNwwsgRgYLtpUhm/dizEnvZXOgc/xMN9pTwHJJ6w34ndNJb36rYfGJdwkg==\",\"salt\":\"Pn49Qj7LqwiiHrIsWaJgDA==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["admin", "default-roles-master"], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account", "view-groups"] + } + ] + }, + "clients": [ + { + "id": "7465266e-fc1b-4c03-ab77-9356ea9a350e", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "d75b6fac-269f-4fc3-93e8-f82daabdc2d9", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "aad6b1d8-1306-4129-a5d2-4eb86e4f04e0", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "eb93fd40-0c55-4e8f-9e3d-262baa8cfed1", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "682e1bbb-e2e8-46be-a696-1467791826cc", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7e801f1c-1dd8-4dd6-bf0a-ef928b5d1b63", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2df9337d-471a-49c5-a377-c56bec0ff59c", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "192bdac4-6e85-4e6f-8f2e-4589e4028db5", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "06406f18-0df3-4c24-9819-74f7af04172b", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "5d077697-a7b0-42b1-a15b-482e8b589dc2", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "a92eae83-6a3e-4437-9594-7ef67b711e8c", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7ec606cb-4325-457a-bd50-bbd5d7044fa3", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "17a2ba41-8a93-40e3-8201-4a79b0ff6895", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "529233d6-9010-4084-aa91-60f0a97ddcb0", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "a98fe901-96b6-4608-8873-654b7fb866dc", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "d056ee6c-f540-43f0-b37d-b332932b858b", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a371454e-3a21-45fb-b634-7b9ee4bb9647", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "806a9098-be8c-42f3-8a29-11fbf19679f7", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "fb480b23-43a2-42bc-b3db-80200e301bc6", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "081b408a-4f0f-41b0-a642-8344a48bbe4f", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "5e7118e2-e2ed-4463-8b1f-3c1b980485c1", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "attributes" : { } - }, { - "id" : "0ddc8579-014d-4eb6-ab53-fde831a6e154", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "attributes" : { } - }, { - "id" : "61cb3fed-151e-451f-b011-56d1151776ac", - "name" : "default-roles-master", - "description" : "${role_default-roles}", - "composite" : true, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ], - "client" : { - "account" : [ "manage-account", "view-profile" ] + { + "id": "1ee50014-2a1c-4061-a55e-828b723858b5", + "clientId": "master-realm", + "name": "master Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "80bd1c1e-5e0c-4585-9d27-a79ff7e4dbb5", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/master/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/admin/master/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "653ac14a-6fb4-4b59-b851-7e1be5350d0f", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "02e4f360-d2f2-4448-a258-7644f54de2b5", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "241ee0dd-53f5-4201-b76e-434f9e9ad67e", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "5ca8d9a0-45c1-41fd-9574-1cab6d0296d7", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "b128bae5-b933-41f2-ba13-aa76750ef40f", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "560a2c01-0691-4698-a367-0b489f12b11d", + "clientId": "sqlpage_demo-realm", + "name": "sqlpage_demo Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [], + "optionalClientScopes": [] + } + ], + "clientScopes": [ + { + "id": "1ded49f3-1c48-45aa-aad5-1a00ca0d34ab", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "49063613-767b-4383-8687-9425936bc7ba", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "2cd93725-862d-4561-b30b-c0cfe69f0aba", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "d4630702-a4bb-4d25-8b40-f2b2b52b9d91", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e1c92a52-ab2c-4708-ba3e-23bcae03c655", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "19017dd8-2039-41df-a14b-fc352cc81b81", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "e675e5b0-872b-4935-aaf7-fff44b165a8a", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" } }, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58", - "attributes" : { } - } ], - "client" : { - "sqlpage_demo-realm" : [ { - "id" : "40ced5cd-0c95-4744-a94a-c3a5dd0fb663", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "60403499-b079-4fdb-b15f-6a945ff0bf0a", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "sqlpage_demo-realm" : [ "query-clients" ] + { + "id": "1163daab-51aa-4712-919c-0d76e84b73cd", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "8b00aa80-3a15-4d5f-9a09-9036c9feb77e", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "44da3ec2-c1fc-4485-984e-3650b7385491", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "9be45694-0770-40d1-941e-1781aa0de39c", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "060db496-5cad-45d6-8bae-8e0233ddffca", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "503bbde2-104a-4073-9e63-96f75bc6ab4b", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "aff3ec1f-91d0-439f-a724-73d1906d6ff1", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "9081d800-5958-4a56-bceb-79b4537b649c", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "be4517e3-4aec-4a67-a1ba-9cbfc0d0529e", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "e80fef90-31eb-493f-b8b6-8c49166b602c", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "7d6236ae-f782-474e-87fb-fb82209059d9", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "09211191-9f60-45ca-93cf-53240c8bd535", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "ed4a7fdb-cf50-4d1f-aef4-9a045434dfb1", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "a9c631d9-0783-45c9-b668-77c93103c8c4", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "6ef0bcea-ecc3-4efb-8304-a7e71afc5727", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } } + ] + }, + { + "id": "3a68440a-2f77-4d91-94c4-ef715dd09ec1", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" }, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "4bda1119-5e5f-47ba-9d43-682db8329ccd", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "8b31c85c-a443-4560-b95e-8b7a83642075", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "37febc4a-1285-43b0-925f-e38faab5e2d2", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "7e898f5d-d3b0-4403-905f-fb1d90642b74", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "dc84895d-4926-47a3-a848-5c82362d23d7", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "d638dabd-fd0b-4a62-84ea-c1555fc3e7df", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "1d39315b-0b1e-4088-a2ca-c47c2a4d90e7", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "fe8dc00f-e512-4d90-a5d0-7f811173f904", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "sqlpage_demo-realm" : [ "query-groups", "query-users" ] + "protocolMappers": [ + { + "id": "a97a22cb-4b2d-4ebb-ab80-ded30fd539d4", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "285a69d3-df85-411c-8547-1b86c175ace7", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "29a6c94a-a5aa-4bc6-a3ff-58960d1d756a", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } } + ] + }, + { + "id": "d9387cdf-022a-44da-a7ee-494f87e8bdb3", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" }, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "52f054ce-8ff5-4889-b72a-75a9a4177096", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "19a225db-b6e6-4a4a-a94d-8f48d2b800a5", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "ee30de19-c3a3-4d91-92c9-665efd0b81e9", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "c52f6f99-480c-4fc8-b3fb-809ac4de2310", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "2a0f02c1-da44-4d20-af96-e572a2565be8", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "130d9aa5-ac88-4328-94d4-3a3ef9816186", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "5fd36a14-c6e5-40d6-a672-4d77f7ce1254", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - }, { - "id" : "72306ee2-fd75-486d-8b97-a841263a076e", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "account-console" : [ ], - "broker" : [ { - "id" : "8d3a5474-6dfe-4469-b8bc-41ae80a0f233", - "name" : "read-token", - "description" : "${role_read-token}", - "composite" : false, - "clientRole" : true, - "containerId" : "df3db4cd-a33d-4ee2-b01b-18045049b41a", - "attributes" : { } - } ], - "master-realm" : [ { - "id" : "bc5b59c5-ddd5-491c-87c7-032490debf43", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "master-realm" : [ "query-clients" ] + "protocolMappers": [ + { + "id": "d89d463d-3fc3-489e-85a2-a0a82f774194", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } } + ] + }, + { + "id": "5e7694ff-87a8-4906-b645-2725c48e3f4b", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" }, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "519edb16-8a73-42c2-8338-ea0615fa15fd", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "a68d4b01-74d5-4c5b-9be7-33f61d3624dd", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "a1809708-2e5f-49e6-aede-26e2554ff476", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "84f1ac8d-79d0-4ad9-a8e1-7230da1a9992", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "a1e8ce6e-6a7e-4d73-876d-1e7f42bc4080", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "2596e057-92ae-476c-9e64-48ea027b905d", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "bf8a6c05-3614-4fef-8554-b303110843c3", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "f8e778fe-eb68-435c-a6ba-20a606b3c76d", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "585afece-a5b2-43db-9000-9c2d8b8efa46", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "34bc0dc8-898d-4860-906e-1ce9da5c0523", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "master-realm" : [ "query-groups", "query-users" ] + "protocolMappers": [ + { + "id": "df22f9c8-f584-4eaf-99df-7bd6a48791de", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } } + ] + }, + { + "id": "ed277102-63aa-4292-b3f7-33d643d3f391", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" }, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "41695096-c0a7-40fb-963b-a0b2a48f7b4e", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "80b05b2d-92e9-469f-9081-d1afc6ddf965", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "daf30043-9627-4686-9b76-9559c8ef4b4d", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "2e9bc83b-807a-4c0b-89b0-b840143e6f40", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "f60c6aa3-ce53-41a2-a5b8-68c0a648d5b7", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "c27d3690-ba51-472a-baac-47d12711a9fd", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - }, { - "id" : "02d6931e-f3be-4e98-a65e-8d591d8ad119", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "attributes" : { } - } ], - "account" : [ { - "id" : "a5beee68-7d37-410e-b91c-1bbfe311c561", - "name" : "manage-account-links", - "description" : "${role_manage-account-links}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "447234fa-61f0-4721-8102-3f98ce445879", - "name" : "manage-account", - "description" : "${role_manage-account}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "manage-account-links" ] + "protocolMappers": [ + { + "id": "13f2569c-216c-4785-b5be-37e7b3207317", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } } + ] + }, + { + "id": "7968ff86-3ee4-4510-b084-143c95547655", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" }, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "c4a7b25b-e306-4a90-baca-fcdb5b8a9a03", - "name" : "view-applications", - "description" : "${role_view-applications}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "d53be66b-ed3d-4bab-bcc2-047118d5eae8", - "name" : "view-profile", - "description" : "${role_view-profile}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "1951173a-4eb1-4453-841f-0fb155cf5927", - "name" : "view-groups", - "description" : "${role_view-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "8ee81e62-4e2b-4b7f-a754-3876355dad7a", - "name" : "manage-consent", - "description" : "${role_manage-consent}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "view-consent" ] + "protocolMappers": [ + { + "id": "c90d27e9-e7ae-42a5-ac6d-25c1601befef", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } } + ] + }, + { + "id": "cfc366e8-a0cb-437f-aeb7-3f20c6c827c8", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" }, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "b1bf48f8-8ee8-4783-8f4b-4befa3478f9c", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - }, { - "id" : "2a77c895-227c-44a0-b046-a36eea529966", - "name" : "view-consent", - "description" : "${role_view-consent}", - "composite" : false, - "clientRole" : true, - "containerId" : "548df36a-4803-4f86-add2-8a11665adac8", - "attributes" : { } - } ] - } - }, - "groups" : [ ], - "defaultRole" : { - "id" : "61cb3fed-151e-451f-b011-56d1151776ac", - "name" : "default-roles-master", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "5f73cb7b-599f-4f68-83c3-b17124e40d58" - }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], - "localizationTexts" : { }, - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyExtraOrigins" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessExtraOrigins" : [ ], - "users" : [ { - "id" : "58e5d97b-9831-490f-bbc0-68f460f16d81", - "username" : "admin", - "emailVerified" : false, - "createdTimestamp" : 1714302502071, - "enabled" : true, - "totp" : false, - "credentials" : [ { - "id" : "27bded41-d568-47f5-a269-f27ab4771e2a", - "type" : "password", - "createdDate" : 1714302502562, - "secretData" : "{\"value\":\"zxGd9GuNE1SzUjftYGfX5ezyW2wGI6NxHgzrIvbnA7oFFlGbScyvw/JGQM7P1WNXvVduMoxoLF89Z6ouj04NAw==\",\"salt\":\"vjBzOJpRplLE5ttVq2jf7A==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "admin", "default-roles-master" ], - "notBefore" : 0, - "groups" : [ ] - } ], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clientScopeMappings" : { - "account" : [ { - "client" : "account-console", - "roles" : [ "manage-account", "view-groups" ] - } ] - }, - "clients" : [ { - "id" : "548df36a-4803-4f86-add2-8a11665adac8", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/master/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/master/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "d9279cc6-cb5b-41c1-8fd7-36e7f60a23b7", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "100d64da-deea-405d-b5a5-56cc99608fa0", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "cad8d965-0689-471c-a8f5-4f7c3b00d9b1", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "b41078e0-72dc-4144-81e8-af081a05edbd", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "4fa7d798-9835-48f4-a0fe-3c3492332330", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/master/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/master/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "0b95958b-e395-438d-8497-039d7218d177", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "b7e6d29a-cb8f-4b44-890a-9eb657afe855", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "1eac3cae-7b57-44e9-964e-08465d791657", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "7ecbae12-e10d-441d-8d3e-58bfeee4fd24", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "c868d4b5-ae14-45ac-b067-b14169485108", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "28900f98-4064-47bc-a5c7-cea8a942b271", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "2161312a-e188-4b1f-a667-4e2de4509a58", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "9a9c2187-e74a-4abf-bf40-a809af28b9c6", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "b58ac8bc-2c8e-45b0-94d7-5d12c6d75d59", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "857c8afc-fa5b-4d99-879c-c57cef6aba04", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "df3db4cd-a33d-4ee2-b01b-18045049b41a", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "74b9c284-7c0e-49df-b2cd-b66d795cf3c4", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "3b7f9e64-5284-4dfa-90ac-a1c3330696b3", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "24ebaebc-0557-40fe-a009-f808ce945683", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "8b808633-3d15-40ae-bead-ca78fe818123", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "b2f502a5-827b-4bf7-b091-a8fddd00a387", - "clientId" : "master-realm", - "name" : "master Realm", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "071f72e9-d5bb-455a-9be1-4fcb1331e457", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/master/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/master/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "889813d1-5d6e-4231-bd20-a4a6f97473cc", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "b1fddb28-d474-48e7-84e2-0162ef1025c6", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "5315dcae-6e46-48aa-a9f9-7e185c3ca07c", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "ce16e2de-8217-45f6-8771-b07df4a1ad61", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "2b89a9f1-a008-4585-ba1b-2f572ff9ab79", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" + "protocolMappers": [ + { + "id": "fadc6eb1-8500-4658-be01-8a5b2d37b42f", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "93e058fa-d051-495a-b103-8c23f7334314", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "bf7f06c1-44b0-49a8-9b2f-ec7404cc5f68", - "clientId" : "sqlpage_demo-realm", - "name" : "sqlpage_demo Realm", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ ], - "optionalClientScopes" : [ ] - } ], - "clientScopes" : [ { - "id" : "1e989c16-9d9d-4f3c-94a3-716818c8dc9d", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "4d80d5a5-95b0-47f4-bf08-71ff6b937aed", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${profileScopeConsentText}" + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, - "protocolMappers" : [ { - "id" : "d9f8bbb5-888b-421f-8f10-827d71fc7358", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - }, { - "id" : "7345ab35-e87d-4528-aed7-6a6706de8b0e", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "6601aaa8-200b-436d-ab91-28ba73293929", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "7078b501-9680-43b2-8453-8c41d8fc978c", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "ee508732-54be-4521-a6bf-5e96a8f00fbe", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "894275e2-3795-43dc-8d0a-91f4c1643003", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "688312eb-1d1e-4970-b687-c2c695019444", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "0dfa839a-c270-475b-830e-d467e62fe902", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - }, { - "id" : "0b4b524c-8a60-400a-84a3-fc9f7cbe50c0", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "b1132b2d-e1a3-4aab-bf17-24adfbf8a83d", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "9beed4ad-1273-4691-8fdb-bd061cc26d0b", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - }, { - "id" : "0964864c-7715-4d62-b2b5-e975fa008220", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - }, { - "id" : "271101af-00a4-4795-b196-a8bd55608722", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "a0eb9ae7-dfc4-4da4-a127-3a8b93b25cb4", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "dc6647f4-0dce-47f9-afb9-0a31bd0d6ebc", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${rolesScopeConsentText}" + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "c38f2db7-a630-48f0-a137-b75240f68929", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "7385872f-3cc5-4621-a5ae-e570c9d2b41b", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "8622f9a0-167b-4bdd-840b-c264aa2480ec", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "852f5a95-ed46-4359-9959-e7647d9ca57c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "93a1f518-fc63-40ee-b333-c53e94aa4acc", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "49b1aede-3298-4e9d-a73e-cfc2e92c044e", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "45b0dc89-a9c5-47b5-b6b0-a0b3ecee5c28", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "5a1bc1c3-8eef-46b1-b602-7471efbdee8e", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "d89b22da-5f54-48ed-9f06-3040c1102334", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "8abdfd18-4dbd-4cd4-9343-ca7533c369a9", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": ["94d5d134-0606-43f2-b870-b75868520879"], + "secret": [ + "Wi2ojXjy9tWmGh_fRtpHlOdkGiT4br9JXeKnKk5xd8gDlO3gNlkGb8HQHlFbU7mLLde2gFFTXdtU1xy-kBvjfb2mohkSVVSp3HrGxlIEnr0DBoXWXDGQbfrUxWfKZMjWTNwHSN1JciQKsw5JIrCkd9MiAH8_xyZuLJxADhRBw78" + ], + "priority": ["100"], + "algorithm": ["HS512"] + } + }, + { + "id": "2a4da4f6-e7a7-4a52-92de-9d8762be6a39", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": ["fccff2a4-532d-48f6-88af-34f519784a8c"], + "secret": ["Qk9RHWnOg_mk2kOtVYwxPw"], + "priority": ["100"] + } + }, + { + "id": "68720305-ee2b-4a0e-b37c-ac8fb63a53a1", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAsXZ7Dh1Uj12Q1WmsowZOSlpo4rTBKSC/ToCRmSwQDhbE5lnGRcwZwzPd7jXes86iFxNuzmZCM/f1XNWBo+/ST87fw85pkxDjmiwHFtqULY0NDwVt6x3Lqm4IujkJ6H+8cREebahRdVIcFhlDCJPwR8kuWNDNgZIeEi6aYPzv8T5tVB4wsIk7jJUNnCkK4tFR/ZOob9pKVPBDIlndLu110OrtU12S2siv0pM7EMnm8sENdLwkATCieNsDixctab2lDNC8BUx5E21AkGBJy7bmukeIpyeippk+qkasTsbkJ1YqLZowTxjq3Ypy0XXGu3t4Yvq92Zr/Ujhg/blimBy+jQIDAQABAoIBAAgGE9/vqo7wNj1ipvLDX7nT5vx3xhNeeYKKpJ/40oxYh4mPaJFnqN3fE/s9G0x6R3DkqN9ogeNzTs2Q574qTeeG3S4vay2Xl2WKn71u9OvZqKFTvQRi7Kj64FEkhWf/kofINDMqvO124Mc3rYS576THRLMGVIG1whSktR01LSzKna+T/mu2yoam8WFoO+Ct9hRT8ejSj4zf3Zf0gRO9JJs4QBmQGjEYABPIm9ZWoiBCLG+TjzaxJzGuR3efwVs1+VowHDkgjX7X9Z3ZTtrqmclznGCOFlBauJCf54sSZoa+BkZezDfu1fjiNaTYHjp3CK4uvJh99INNNIFjgP+joT8CgYEA1fTeqEEP/l/yCgaqUvFYjoGkxeMiao9ptyXxNe/anbqyHrMUzCMtyRsEXznwGefcBTn4qiUecwRNxBa9ujTWTBVNvd2ax+R3NW5R1QVjOiF7KL5q/TjmrtM2PWmaRiwN2bgxnMQAtf6aKf30xSqQ6lFtn/I6REHf60rzKQixQosCgYEA1FXJcl9i0DjSLq3DCBh6aVmh8eebfYECCRXuB09asbJOHm1aFzSDqS4PasS16EQa1r9SPvXbVcGwhuzV41H9bh6QppasGD8BaGIYasMtYlJ/LpQEJgum0bqZmRnLCnrs4QxVKKdpp9BHdCXuFjUDc+MJAro/OUrNFGG4sxNVHkcCgYAKeRoNEmI+CWRHqVvdA4NaNm5iYWPl429BT9Im2b7Rybm+VvXFqFMtbO0h3CwsmHTkrJnHelmrN6K23oYa/0seHkzX5mkVL9HGA8htrP3Wcp0cuXVzP73LAPu+tdSfarii16lWCyIdxoC1XYEFxbeiQKolEi5X+QGE+v48G/jRUQKBgBJi91WzEtBrCzBFlazeycLToyVaY+mDQVTeFEWHxpe6k+8okvONdZUxyt34+LOLKjPMT2fqTDrp0cptObw8flCJzwbN50sWMZ4DWI/uJMDt2duDr7RHsANbQC+0vxNCP77hHYKutIR2kalqG2rK3mirkT0uOYlRg96u85p2IxnDAoGBALdM+45DTkGUmTtVtrI26xvmL0Mg/fUUBkErMPpfb4Q4dRaBq8n2G5tgJsJ+tY0tySp1fl834SfUeqALuJPUV6pUtkegfRB3P4uiTVlYJ1vaoANO7aLgCnxsH1WbGgVCm0BY5KFuwX/bKvHAp6kgfUIvNrVYwvCkkZm5putSCBM2" + ], + "keyUse": ["SIG"], + "certificate": [ + "MIICmzCCAYMCBgGYVtX7PDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjUwNzI5MTUzNzAwWhcNMzUwNzI5MTUzODQwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxdnsOHVSPXZDVaayjBk5KWmjitMEpIL9OgJGZLBAOFsTmWcZFzBnDM93uNd6zzqIXE27OZkIz9/Vc1YGj79JPzt/DzmmTEOOaLAcW2pQtjQ0PBW3rHcuqbgi6OQnof7xxER5tqFF1UhwWGUMIk/BHyS5Y0M2Bkh4SLppg/O/xPm1UHjCwiTuMlQ2cKQri0VH9k6hv2kpU8EMiWd0u7XXQ6u1TXZLayK/SkzsQyebywQ10vCQBMKJ42wOLFy1pvaUM0LwFTHkTbUCQYEnLtua6R4inJ6KmmT6qRqxOxuQnViotmjBPGOrdinLRdca7e3hi+r3Zmv9SOGD9uWKYHL6NAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK6WL6N+Sq3gO77b8Z5GzP8ut+6v3Der3zAh2aUlPkLEB8zmAfygsHFilqgddzP+AQduc1wXqLzJzhvgN2Im48VaLzzog+5M1KGXYrKP5ifeQ0cKQ1jqrITvOfLROsEnaR0glwpy4rQjgwU/e55SLn6B7lcng1/3uOrsCEo02cqVsREhOWLX2gS/2aG1lBbNlFS53RfFhstLn7DtWKAwxFPIZ+jEft+nZMSN+R8/Iq976/z13lf7p08M8cJ9ZeuAGIkcsLeT2YaRhKgjNzgKahb78zBXMYtI2rg1uMPthIoJWhasktabYCsb1JW6Xa968Ic8zHIN2XTBkj/ZhG6cFj4=" + ], + "priority": ["100"] + } + }, + { + "id": "6fa1bd11-30a0-41f9-be42-37acc553c765", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAwSejHG9SftAI29iifSmqHXizhBZ03G/5jCI2G21BpJtCAHEZPTPeFqYsEKh9xmKJ8ri2vz+/Zej+h/TJDAe1E+8OapA0xcbvukK3AyBkBikydLBoyIyAi81hvZIiXxzFj1WPaKBy1r4//Od3Cz1WdwTJZbODCO36mFLCP6MCoyUNqlqxv/lKD+Q3QdU76kyfAIIcc2BtDTabWjlN32BWFSPu405kV4DZhj7HeR/nO/uVZzO58zguzFyI3llGxVjs+Z4Iu0L/OUok8HAdCOD0rbJSPpfSQmtc5X1lo1n7uUNyYEjgkwGhMsMKenxXgOmyVfr2PMbdo4chVpDdTbCAWwIDAQABAoIBADm6AWcVpCePQB6IQ6O5iIRvVuNinMFm28N0VAxlXkl2N0cPhhYDAtxtBF1kJdbdC1JVvxQwVqD7/dofH9jvEsCm4P2bJJJ5TpsxpiWSkCJBPLmgIWjSiPZ/RrdTzd70J90bGpWP4lJooJISkUL0LXu7m/8/o6lPCmZs0W4YZ0jfXKBYVZl1ODAgrlbimYbbL9aVr5Jm6z+uY/4YOXVwqjd9DQJcg9nokPIq1k3Xx5EEP09RY8bZ7AqfG0vYtLlEP9Z4MxVjDCn7kXer+BLnS3hVQP6JfiolJj6TTZYoKRJ52aT4EYKwVpgkwKn2sArrputUFtvuYhBgT8T4mbUfmLUCgYEA4yIeavLvCBTbw/W2+ce+l1vv9vA7bOgI38dXVIkOLb4SlSNE9TGXOZcoKhTHRtvKe+iwZqe1Uc3ogqA74gCY7T9N4DU2dkbhmvWfg2FiXT/5xcgDQaV1EfbxXLBPywTfJvR2WLht04UiS9tpJFYzySqX7bDTC3nwc8BNTYM9RA0CgYEA2bQDwvozN8muebHAyarSe414DL442i5zPOdiq1T5G6qg9117AVA92edkjzpgaTWAFdITKyPcLGvRACIG4RYczX78o4vHTs25RkDTi1m/evXAvUvamwjrBWUw/f8+geN6cMc7luTv/0KoGWDSzYMBA7IdWNY6NEVOPbBmR8i1NAcCgYEAkcWa+g7SJECmzvyLI4Hzq1bBCp4htYKx91ULkmCn7emYKYlKP4dFRBvkFiXhw3NaX+32ENw/vbHGMNe/twulGlbPlz7vpjdVoctURdChfbGKj0oP9PjIyu/O9ird+zE0Ot8YeVZcfi1q1n6J211LvScN/OnIeQwYq2FW+5FoJ50CgYAcgTKA5AuywUiEDJ8miKRYoxRV7s442x4hmlZUAqM/WR8MZIQHjv8aOe7zxfv7qpKjyMbTvjVE57UM5GesLx4EVh00OMgW7F7W8QQB2fV1XxombvknlYpYQYChsTr4/NT6UUvfHQjDjnG+KOxRFlcaqcan7Bzg3TY6Y49w1LnNHwKBgHyM+2woIBINOa3RopErgWha6jJMe37r/3Y1d7bGYboqojgfGQwXDrxjLy9pt/jnTGJhTSOcZcoT1/un58rNDAV4aL+zuIq2JQOsESWt+kTcVBJvhGfM2ou+Cj65EReXONLywHM7KxL+fEwCySU7Iyugyvp3kRJmll3ff38AySAo" + ], + "keyUse": ["ENC"], + "certificate": [ + "MIICmzCCAYMCBgGYVtX7qTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjUwNzI5MTUzNzAwWhcNMzUwNzI5MTUzODQwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBJ6Mcb1J+0Ajb2KJ9KaodeLOEFnTcb/mMIjYbbUGkm0IAcRk9M94WpiwQqH3GYonyuLa/P79l6P6H9MkMB7UT7w5qkDTFxu+6QrcDIGQGKTJ0sGjIjICLzWG9kiJfHMWPVY9ooHLWvj/853cLPVZ3BMlls4MI7fqYUsI/owKjJQ2qWrG/+UoP5DdB1TvqTJ8AghxzYG0NNptaOU3fYFYVI+7jTmRXgNmGPsd5H+c7+5VnM7nzOC7MXIjeWUbFWOz5ngi7Qv85SiTwcB0I4PStslI+l9JCa1zlfWWjWfu5Q3JgSOCTAaEywwp6fFeA6bJV+vY8xt2jhyFWkN1NsIBbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIEARIGo/Wbp3lQodqWqzVtmKwEmwBPMEtw5P+DrtT5oFagtuZUf3FgHGvBzfe/K6TvqayHvmxKGXm/Z0ZYOfwtdHu8/EDblQMSXHuU41ta2oPy8CEGjXfqSJtZ7z0lE8mNZyxHJmMTVJGEB1isGE0xt294kjj9n60I86VMGyZe1x2ZF0BN/TWgQ5cSZj65qVhAVK0xaJaVkvQ7T0AsEF7JkumUNDHjuz+t4cHoSV3aPSkT4vojvbRLdBKn7BqEok3McU5uuqgPPNWXUQQEIjdK1rb2oVY4/t2iwsKssIzEdsdpmpr/HDsgJ0EUuJdhyOI6qoydGG0pAAqZ04X3xPXw=" + ], + "priority": ["100"], + "algorithm": ["RSA-OAEP"] + } + } + ] }, - "protocolMappers" : [ { - "id" : "d9494cdb-c2f3-456b-a537-87206389168d", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - }, { - "id" : "bfcdf78c-4736-46da-9380-630c90b536f7", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String" + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "afc1d656-9b81-4b5f-8de6-0116fe92a5d2", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "260f109f-8f21-4516-b7f6-011bbec65277", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8c854d16-d132-4d3a-a94b-1e6bec1028f8", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "268574ab-2cd9-4746-a029-7160912c76ae", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d15f8bfa-e5b8-4428-99db-ea6ab9a760b0", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "f5a7b666-8615-4e00-b878-7f8e735b5d5c", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "62214974-de92-4735-92de-331fc1305d12", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "5e6c50b9-3c96-45bc-bcee-e72a15724a76", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d39d7e6d-916e-4fa0-affd-2d1045c856bb", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "9159381b-6b1f-4ab4-9a46-51545f05b03f", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "6b61b414-ad49-4d73-9b91-83046f498625", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "b2bf7615-af7d-45f1-8d03-c8f7c4ad3461", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2ee49771-5af4-4012-81bf-3372bc9fa9c1", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "eaf3d406-e280-4af0-aaec-7e4876a6d739", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "efdde3fe-8169-4140-9f58-4d2d2395b88c", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "896d8a8b-a228-4a1f-aa99-84fc4a7eabd8", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e52b9c9f-9685-441e-80b6-bf14cc864a90", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "571378fc-6b41-4d83-9450-fca07b0bc6d6", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] } - }, { - "id" : "8126f072-2927-44f0-a9b2-acd1dc6380b2", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String" + ], + "authenticatorConfig": [ + { + "id": "563c9f81-9af9-40f3-b83c-7174d367680c", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "30be6cc6-ce8e-4ea1-8004-a090c944bcd7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } } - } ] - }, { - "id" : "795915bd-d6f5-467f-a2fd-f828090e778f", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "c213dbf9-fdd7-4a8a-9a9e-9440c06ef513", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true" + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} } - } ] - }, { - "id" : "2cea4163-775c-4943-800c-fb65df65e2bb", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${phoneScopeConsentText}" + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" }, - "protocolMappers" : [ { - "id" : "7a93c95d-1d84-4277-b44e-ed57761b8bf4", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" - } - }, { - "id" : "3cc9adcb-1a09-424d-a7c9-b8c4ed6f98d7", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" - } - } ] - }, { - "id" : "ec0a170e-3020-4c72-bc82-035757e5f9fa", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false", - "consent.screen.text" : "" + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] }, - "protocolMappers" : [ { - "id" : "64ae245b-f699-4762-83d0-9d85d8205065", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" + "clientPolicies": { + "policies": [] + } + }, + { + "id": "d7757cae-367b-4dfa-87f7-a19a789af2b9", + "realm": "sqlpage_demo", + "displayName": "SQLPage Demo", + "displayNameHtml": "
SQLPage SSO Demo
", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 60, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "50468abf-dd38-4957-8e16-d7b1b8345a19", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "d7757cae-367b-4dfa-87f7-a19a789af2b9", + "attributes": {} + }, + { + "id": "5c8a5401-1d48-467d-bb5a-f9b8b07ea281", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "d7757cae-367b-4dfa-87f7-a19a789af2b9", + "attributes": {} + }, + { + "id": "3473c742-cfa1-4d81-975e-0d74bdf56795", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": ["offline_access", "uma_authorization"] + }, + "clientRole": false, + "containerId": "d7757cae-367b-4dfa-87f7-a19a789af2b9", + "attributes": {} + } + ], + "client": { + "sqlpage_cas_demo": [], + "realm-management": [ + { + "id": "16d9a55f-c85f-4e81-88b1-fae50781e9cd", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "356acf1d-dcae-448a-ad91-1eb7e87d5a66", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "52d49f80-5549-471f-ac67-5eb50b047405", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "5142283f-9900-4ab8-9759-19ec861f0b4e", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "view-identity-providers", + "manage-realm", + "create-client", + "view-events", + "view-authorization", + "view-realm", + "query-users", + "manage-authorization", + "manage-users", + "query-clients", + "manage-identity-providers", + "manage-clients", + "view-users", + "impersonation", + "manage-events", + "view-clients", + "query-realms" + ] + } + }, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "31ded332-e8e0-4bd0-928a-aec9c2917edc", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "bf4d4971-836b-49bb-bf66-5d07992bda20", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "9d665e00-274f-4389-ba0e-a8a8f7eae5fe", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "d4d35d92-798c-4740-bf10-6ef454cb954d", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "053ad20f-3570-4e1c-af3a-6d275236cdc2", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "65bdc99b-ab0d-4693-b089-648d75650da2", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "3c21aad6-86eb-48cd-a700-81e3195bfb58", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "6b1169bb-7bd1-4154-9ef8-d104c94dff04", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "752ec963-dcd1-4558-85c0-9c606f5d1181", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "10befa18-80c8-461e-bcea-5dba1d864cb0", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "c9991186-694b-4758-8e1b-ec6aa334ef4b", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "b65a8c12-f5f8-427a-9d90-eccf22d847a2", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-users", "query-groups"] + } + }, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "57dbd22f-cb06-47fb-a0d3-d7bd7f715595", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "d533dda6-e2f7-4163-a0b6-4752cf74bcde", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + }, + { + "id": "439fe1a2-c13b-4043-832d-00558a53ec6e", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "sqlpage": [], + "broker": [], + "master-realm": [], + "account": [ + { + "id": "f97398fd-8608-46eb-ad58-39b92dee69a7", + "name": "view-groups", + "composite": false, + "clientRole": true, + "containerId": "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", + "attributes": {} + }, + { + "id": "722736c4-47ce-4a8f-a74a-38b37f172bc3", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", + "attributes": {} + }, + { + "id": "9cd94925-88bd-4cd2-af60-797da71aa1e5", + "name": "manage-account", + "composite": false, + "clientRole": true, + "containerId": "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", + "attributes": {} + } + ] } - } ] - }, { - "id" : "ca1a8718-ad1d-4970-8b2b-074326cdffac", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${emailScopeConsentText}" }, - "protocolMappers" : [ { - "id" : "212c3ce9-b512-466e-86b1-c58b5440aa90", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "6ba8694a-ec0f-4684-b82e-d36c4ee04139", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "2a9f50b0-fcd6-4f96-98d5-d311693a379f", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" + "groups": [], + "defaultRole": { + "id": "3473c742-cfa1-4d81-975e-0d74bdf56795", + "name": "default-roles-master", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "d7757cae-367b-4dfa-87f7-a19a789af2b9" }, - "protocolMappers" : [ { - "id" : "c1f53b10-2a8d-4191-b4ae-24cc07a2f78f", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" - } - }, { - "id" : "90c9e9a5-cdb2-40b2-bd2a-bce5c0924721", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "0cc0472e-a38b-4f45-8d91-77ecfe5c8b7d", + "username": "demo", + "firstName": "John", + "lastName": "Smith", + "email": "demo@example.com", + "emailVerified": false, + "createdTimestamp": 1714079479552, + "enabled": true, + "totp": false, + "credentials": [ + { + "id": "d453f7cb-5ba5-45ab-a694-38ddffb93503", + "type": "password", + "userLabel": "My password", + "createdDate": 1714079498525, + "secretData": "{\"value\":\"gxi8oR/w6GPvZjUXJAsxSuxWZCDsxL3hwzjlfymoeYsRLXxJIvJdy5SeRch4BOYwNdfRwrbOenBGScCleyQkfA==\",\"salt\":\"HTsvAP/Cig6pIOVo3SPFnw==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-master"], + "notBefore": 0, + "groups": [] } - } ] - }, { - "id" : "4f873674-3e7f-4dff-bb20-83060bca777b", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "839733ca-3361-46c9-9782-2204afb5a876", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] } - } ] - }, { - "id" : "be0ed55a-e21d-4362-914f-b32a5c6b4dc1", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${addressScopeConsentText}" + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account", "view-groups"] + } + ] }, - "protocolMappers" : [ { - "id" : "921bba46-dbaf-4d54-8b23-a8a40d55ab1c", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "introspection.token.claim" : "true", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "xXSSProtection" : "1; mode=block", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "components" : { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { - "id" : "ca563eb4-e8c7-4364-8211-9df69030c180", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] - } - }, { - "id" : "82df5247-f222-4828-a045-b6b0a130f3a9", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper" ] - } - }, { - "id" : "6c34bd81-cb51-45df-86a2-a36d0cd74ab5", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "84cba6d0-c941-4ab9-977a-7831b89b4d20", - "name" : "Max Clients Limit", - "providerId" : "max-clients", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "max-clients" : [ "200" ] - } - }, { - "id" : "5cacf453-2d45-4d67-ba19-92d3f130625c", - "name" : "Full Scope Disabled", - "providerId" : "scope", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "b1192031-af2f-4e6b-9e8b-30c372b89d09", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "a717f6d2-58c6-4cc7-8868-18da27d609b9", - "name" : "Trusted Hosts", - "providerId" : "trusted-hosts", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "host-sending-registration-request-must-match" : [ "true" ], - "client-uris-must-match" : [ "true" ] - } - }, { - "id" : "d20549b0-60cf-48a1-b1b2-d45cab6e90bb", - "name" : "Consent Required", - "providerId" : "consent-required", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - } ], - "org.keycloak.userprofile.UserProfileProvider" : [ { - "id" : "3d2627e3-2255-4bb8-a6b9-53bbf5a59a81", - "providerId" : "declarative-user-profile", - "subComponents" : { }, - "config" : { - "kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" ] - } - } ], - "org.keycloak.keys.KeyProvider" : [ { - "id" : "11702cc0-460b-4ea1-86e1-1eb0beb5a7b0", - "name" : "rsa-enc-generated", - "providerId" : "rsa-enc-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEowIBAAKCAQEAuX9v1GCMqzHw/PVLCr4G49R0FCgc1CkJvDi3xlzOtZbFR+JCAIlzvBVxut4HiprKKaYSXGiqGuqOXDrPVQF8Dr7gACklh310i9B6ex1ASMgHf8H0JB6TzD701kuLZ8H5J6doPvYWu1D32K688Mv4SXrhMeembtGaECz1FfGETF1rE5wDfF7/2KbN9mFK809Zk/Kk8QVTBdX5osFz20QLsV+RAFNagTvaI+Bn6pSQboQAJN7RuvGSQnxBzw7x5hWWdDLlsHXXheOb8NlWTwbpNXws9SfB0Q3cqI7zk/tnX9L7Lmf8VyOpT+mBEf5ATQDx0/DsiUYJ9aZ/pXr1zyOemQIDAQABAoIBABBMjMh19VZDjJ/Fc+MNQHC7ZbmxrprLwlG6EBfLZtRY25vN2yvN6A/yOGyJftMkd7gCIiHhhnTYAEtFVZtsBnkLN4Z8FxKZKOjyGBkCQHhErzgAFFIqJ6VuMLg1qhmjVx+vW38Qw32RBz4QWIS1/2qBdBEdAChDnYxnTt+vOYc2FbAT2FbP4QdXIA+kit7eu3Zn6pf2Lwp1bHREk45JnEb+0Pk817nyXM7NoA01/nlorwK2cOTJcXj469r/Dt7Rq4z5Mclzwc3sk84msATDVEiWXFq9uM3LriwX5F86cMpEJZFgs9Kxec6C45Q9cWPVNN489Mmo3qe6CPg4yj6EM0sCgYEA719Z3aHfKr08fGIvMqoKrmEkbtPmFPOngs0OOZCf4r2iBf6Rz1LQxc02rdt8MewvWVY1/c6s41EeqOkqooX2r4hpDdPmSKMql6bfLTEfRhwQXTE8tjOF0u2jQ/E6HHph5dW2BwtK3Fy1ydxOm5hLi04k/335BDRkAXCHWJUjeRsCgYEAxmIOyFsDEt51kgc4lbkGrqjuOEDJIYvQk7NP4FdCZfaaiQay5aLu/cq9mucPffGeioP/bGWeRd/xjfcqtkvvlZrHV5PDF+98oqGT/j29+tCS0wc+TztwnHHxd4bjs2GFc51HXa8D1YIkQ1+yMZ6FT/azUYJ1TtBpKTe9pWSR1lsCgYEAme4XPA78E9/dJZ99+naep8SQSTS6oH6PGe4DV8FJD+Yiavhpi4kV+B0fLG4YT4IycKCf17cwNn3T2YsXf6vmFrsB04KQc+V5XkxQ+B6+nbcuTr966y/2vs1SUD6vv+BzU/k6NQ1t9ocn38dsRWKKMerun93CZJHMPaC/aJd/WEECgYA/tcgXkhr3mVfasNT8SHKb7waqLCwryNIQTRrO6lcZlcvlS7ByTRRBjzW7JI92G/2VcHS8JBpjSFs0A3fFGbf0rMNuHbce3buCIy/7C2sSvJ8ahI3/NhChPXqVr007E4ufKJh9vrIVhHkO5hG9G7KmjoziFWqFCCttoEgYcKbhVwKBgEdq1CPEAIBUFcl6uagDRq8ka2IqOq/+xqtMIT28tnpNoZYnY26lAmybWpgmAn459SuPl+6g9B/xNPsUU3HHg1wxSReefzsJRtRmmdZvI/mkaXl/9FNNvFP8n0dufnxne7831jhgNC/zsIJX41GAlfhQBlVtJQFeanJO+52XdZui" ], - "keyUse" : [ "ENC" ], - "certificate" : [ "MIICmzCCAYMCBgGPJGQ62TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNDI4MTEwNjM5WhcNMzQwNDI4MTEwODE5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5f2/UYIyrMfD89UsKvgbj1HQUKBzUKQm8OLfGXM61lsVH4kIAiXO8FXG63geKmsopphJcaKoa6o5cOs9VAXwOvuAAKSWHfXSL0Hp7HUBIyAd/wfQkHpPMPvTWS4tnwfknp2g+9ha7UPfYrrzwy/hJeuEx56Zu0ZoQLPUV8YRMXWsTnAN8Xv/Yps32YUrzT1mT8qTxBVMF1fmiwXPbRAuxX5EAU1qBO9oj4GfqlJBuhAAk3tG68ZJCfEHPDvHmFZZ0MuWwddeF45vw2VZPBuk1fCz1J8HRDdyojvOT+2df0vsuZ/xXI6lP6YER/kBNAPHT8OyJRgn1pn+levXPI56ZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEmZQNZzEYJ1a9Sptd0EIXv94O/GI/S0bMTkL3BcFV6IaUb0Ng9YkHM+1gflxtw4EJTWSw3OKWnkZeW77FL8ChTIYLZMJkt+S6nzOBUm0ll9CXrfGMypPi4TZTyimSxj4opfRmXzWkTiq/5TipG1YMAmxAGk5/YF7+pqaqQqAN+p6owovOPkVYDAt2rMltE90fl4Ib28NPpCaNcN4C68SlSCNSxEzDUZCmCNTDc6DQIL8b1RxRgDmJLb3dVMHndBZtAVRnDOYittS+Ii9ctIkIc1teRvtMU+XYgfsMR/SR7qPcUYcL8nE/oqvaDlBRfPAtGbBHj/8/xDO69LdbO+28g=" ], - "priority" : [ "100" ], - "algorithm" : [ "RSA-OAEP" ] - } - }, { - "id" : "2284aa90-0cec-498c-93cd-5130f2c27913", - "name" : "rsa-generated", - "providerId" : "rsa-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEpAIBAAKCAQEAxAqWmKSn9YgFLU8xpvCKQ/HnboGdARvb9VOhOYTpGXoerKOqs8FyAqnZBo6t0i6hzW5zptDSZN9bH+t5Crx0dYU+cwYlnY4qx+tcyKVeU3qS5Bo1g01LDFiOk9UsB/VxarNuq4MJGU6sG1PNUwzE/mrutehq47IjxdaUbNQ3fSBU/8sIF/KaemWNqWfFAGQyr668YmipEVqkm4OxLDmddStvQ5E+KNDqiPttpi6AMToiY/Yzw2IGb9xHMQYMh/UZzBk892ZX5rq99NJ8I7TYUb/9w6PhxkTtvyHL3OdpogMAuh7Ak6wtCqDy1IN5WDH3kFZAqEQ4/QJtWIJmdlGHYwIDAQABAoIBAA+QtadV/333uE2CKbOTCq9dYB4+jUw+vYarhrQN8QIUV6YmcpHGLLq4aKJkJrn2+OLaP7WBhAr9TvtoU1p+XxLLqwsv9EGfvZbVAa9ureAb7djNme0ccHaA+4USWmYhX0l1CLb2dvz66aBb1YQgEe4PaHeqBf6hMMTvBibj7KnzCRKpcsXCuKR2/g3yKbUxE3UjDyrl7Wvoz8lmF+/lG4iKCV/0EZb1+yni0amBPQexZ6VHEDaAhGEpHAQH7nL2uclIJrfmBdechBIbdswZIftvPBwbeJ3bQhyorYkZiFfpQ1b4iBoYrxGFfXaSchRDxokZEweecrhYaYuglQ0quE0CgYEA8H46Yck5CNmrjVIIeLLFT6mT5AUzCTPiEJx3ZsTGhekOnRo+h+lcDziEXfV8l/TSPt/gRdzg8y2NlKdA0XsOJcZp+u+Ni2RAUg0ce6/q66piBfajg7IwdCFCrOIbpQllKNyWLjP4QnZeQ77d9TrzRaxYE2F2ftMDXPxQSKbApM0CgYEA0K6avQbMV7A95jhqjofIbUiLo/IzLGgACMGuCTfAELOdZzsWEtxrCR+2CCvwoelKIdDZK7B/mumtqjE3nb79M+35cq9He1Oyvm2ZxFqzj2V0xNb8DTsGrTqmddlc4RWZFVOKVtzr3two9E13PP0Z9D1nemizr1nJDN17HfI2XO8CgYBv91TXEggrxqvIL9kh2JlkbV8dWg5Yyn8FnNM6VYLX9ZmIKx/RxyPFMlruI8zOMn1wKGsSG7Pfg+XWv8e9v7zmeWn+Gmmb7CDGErSgIVcOQVVB0YJvXhaQ+qtAMkUMoUMnswt4l1mOF+3rPIG6GqgIWCTpYQb6JOP79ryqFni6QQKBgQChbpERrZKiOxqHYBk+TVgFAlvT5eckcQelvn5tbw1LXkdjtlerG4xJsJhW1fb+qcPJSRDXNWBhbGgGArDtfPPPGkcgKuv3QZZCrfKH7Up+oZOlOlIdwg22MGrZO98X2GdmEgwwihKFgnbEFrEpIvrS8DV+gkSOX7yiESvOzLK1TQKBgQC5D9ibFZL0NVojcYdzJgemtCX0pKAgyQHsXRj91sA3dNe4JA9CPWHQO/OmAU/hMb0JcHVrQ2fJIZgKw+08eklrS1wFITT1LR1vCJChixVvJmBxfTrD2i9Q5bxE1QQqOTlUH29n4CGsD+X2l/vUyS7Yu6U0tDfYIZy9OdS4hCaKiw==" ], - "keyUse" : [ "SIG" ], - "certificate" : [ "MIICmzCCAYMCBgGPJGQ6ETANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNDI4MTEwNjM5WhcNMzQwNDI4MTEwODE5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDECpaYpKf1iAUtTzGm8IpD8edugZ0BG9v1U6E5hOkZeh6so6qzwXICqdkGjq3SLqHNbnOm0NJk31sf63kKvHR1hT5zBiWdjirH61zIpV5TepLkGjWDTUsMWI6T1SwH9XFqs26rgwkZTqwbU81TDMT+au616GrjsiPF1pRs1Dd9IFT/ywgX8pp6ZY2pZ8UAZDKvrrxiaKkRWqSbg7EsOZ11K29DkT4o0OqI+22mLoAxOiJj9jPDYgZv3EcxBgyH9RnMGTz3Zlfmur300nwjtNhRv/3Do+HGRO2/Icvc52miAwC6HsCTrC0KoPLUg3lYMfeQVkCoRDj9Am1YgmZ2UYdjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHP+2Vmm7ZsnYswkElFryNUafHQLqxDHeJVTSzl3o1pHymBsOOn8mIUT+7M8yuJGzRmqjBJ1+QszwrtCQWpSWa48Jk53kzKyOMJg7dQclhMI12LMLR0fNKEIEtaA52brRmxoq5/F8SB20gP4ugdquMudwOFmxFRGgVQgq9MkkjXk0LvwVDMEoOj9fWb1G0uY7+ktngv5FviFgdZCqNL2A6Y47B2t43id+878vY+f4nMVd06+0I9Yuwi7y9YNiHTIZvXkmHVnAPh4Tob9Ljlby8WGDHPZ3A1oeCh90W2trDlgiuq3faVfNMeinRBYHTc2R77DwCPRLS/QfOyvQ5KjtdY=" ], - "priority" : [ "100" ] - } - }, { - "id" : "ed04572c-c252-4e71-b681-f3955717dfef", - "name" : "hmac-generated-hs512", - "providerId" : "hmac-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "e78efb62-2382-4c6f-8479-fae84fe8538c" ], - "secret" : [ "-p6jGVJYWlZGctRS0OifZczwGLYc6P-I-Lg8FDWXp4Y5uPJh5WV9elt7pkfNkYvvxLLsg1aQzY6XA6pwFUlx7GKDfWd-1ZTnKWKSTiXuXyD6Zoorp_Pnj3m-v9HzbIKFeU0bXu7zUcF7mTJT1_Xy8Fn_YQQGbU83G_UF1OtJPIA" ], - "priority" : [ "100" ], - "algorithm" : [ "HS512" ] - } - }, { - "id" : "b03b058f-f513-4d14-81d8-e5bd005df5eb", - "name" : "aes-generated", - "providerId" : "aes-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "2f199889-4c18-45d6-bcd9-4f580cfb20df" ], - "secret" : [ "Pn6-Uh8H7PtYBRBZVmG1sw" ], - "priority" : [ "100" ] - } - } ] - }, - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "153b5523-48ac-4764-ada1-48946acea8e6", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "f07f8268-e9fa-4d6f-bf3a-31d902979c2d", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "fa86288d-47dd-4bf2-abe0-8254a9e46459", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "e8cf1884-9090-4936-a5d5-1a2fedcbce0f", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "1c36144c-9533-43d9-8296-43c8ce2234b8", - "alias" : "Handle Existing Account", - "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "e417c76b-0538-4313-9681-7225ced3083f", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "103797b4-b973-47f4-8160-d6a5fa8467c1", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "43e83b31-5728-4e63-9bef-aaa7bd2535ac", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "69a28597-4dd6-4845-b681-4292fe62cd7a", - "alias" : "browser", - "description" : "browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "ffcbcf48-fdaa-403f-bd47-2aa255c135bb", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "1809b97f-d71c-4e41-bd03-aa25f2b28597", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "3e749926-e184-4e9e-92b9-05a71b3cd447", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "dd10f935-b2c7-4053-b05c-fbb6e04ce873", - "alias" : "first broker login", - "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - } ] - }, { - "id" : "5a3127d1-3932-4e39-9dac-a8a72ffb0913", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "8bda7b68-79b4-40b9-97de-e506dac9cb5a", - "alias" : "registration", - "description" : "registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "463cdfc2-8c48-45fe-8137-e2dc855befb0", - "alias" : "registration form", - "description" : "registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-terms-and-conditions", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 70, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "eced263d-5cf5-4d2a-8c30-a4edd76c0ff3", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "399c42fc-6065-41b1-9798-3a55e1d6945e", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "a8ba6485-8230-4ffa-b90a-82bcfd8d22e4", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" - } - }, { - "id" : "f613bf6b-c671-4e78-b97f-5920abecb690", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" - } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : true, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "VERIFY_PROFILE", - "name" : "Verify Profile", - "providerId" : "VERIFY_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 90, - "config" : { } - }, { - "alias" : "delete_credential", - "name" : "Delete Credential", - "providerId" : "delete_credential", - "enabled" : true, - "defaultAction" : false, - "priority" : 100, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "firstBrokerLoginFlow" : "first broker login", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaExpiresIn" : "120", - "cibaAuthRequestedUserHint" : "login_hint", - "parRequestUriLifespan" : "60", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false" - }, - "keycloakVersion" : "24.0.3", - "userManagedAccessAllowed" : false, - "clientProfiles" : { - "profiles" : [ ] - }, - "clientPolicies" : { - "policies" : [ ] - } -}, { - "id" : "d7757cae-367b-4dfa-87f7-a19a789af2b9", - "realm" : "sqlpage_demo", - "displayName" : "SQLPage Demo", - "displayNameHtml" : "
Keycloak
", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 60, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : true, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxTemporaryLockouts" : 0, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "50468abf-dd38-4957-8e16-d7b1b8345a19", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "d7757cae-367b-4dfa-87f7-a19a789af2b9", - "attributes" : { } - }, { - "id" : "5c8a5401-1d48-467d-bb5a-f9b8b07ea281", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "d7757cae-367b-4dfa-87f7-a19a789af2b9", - "attributes" : { } - }, { - "id" : "3473c742-cfa1-4d81-975e-0d74bdf56795", - "name" : "default-roles-master", - "description" : "${role_default-roles}", - "composite" : true, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ] + "clients": [ + { + "id": "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "89f96859-de46-4778-bd0b-e44256ff8ea2", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "7a8f7d8b-93dc-4f5c-a8cf-2caa9802ae35", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "853110df-77fe-49b2-b8be-000ae4922b88", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "12e0887d-af61-4066-9d29-a54f2523606d", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, - "clientRole" : false, - "containerId" : "d7757cae-367b-4dfa-87f7-a19a789af2b9", - "attributes" : { } - } ], - "client" : { - "sqlpage_cas_demo" : [ ], - "realm-management" : [ { - "id" : "16d9a55f-c85f-4e81-88b1-fae50781e9cd", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "356acf1d-dcae-448a-ad91-1eb7e87d5a66", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "52d49f80-5549-471f-ac67-5eb50b047405", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "5142283f-9900-4ab8-9759-19ec861f0b4e", - "name" : "realm-admin", - "description" : "${role_realm-admin}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-groups", "view-identity-providers", "manage-realm", "create-client", "view-events", "view-authorization", "view-realm", "query-users", "manage-authorization", "manage-users", "query-clients", "manage-identity-providers", "manage-clients", "view-users", "impersonation", "manage-events", "view-clients", "query-realms" ] + { + "id": "ff46bc2b-4c0b-49b3-9bca-0938b05a9581", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "46a75d57-9af5-4466-8e0c-deb66d6d58e8", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "12f13df0-1a10-4792-b62a-c67b3d0f9bb0", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "31ded332-e8e0-4bd0-928a-aec9c2917edc", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "bf4d4971-836b-49bb-bf66-5d07992bda20", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "9d665e00-274f-4389-ba0e-a8a8f7eae5fe", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "d4d35d92-798c-4740-bf10-6ef454cb954d", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "053ad20f-3570-4e1c-af3a-6d275236cdc2", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "65bdc99b-ab0d-4693-b089-648d75650da2", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "3c21aad6-86eb-48cd-a700-81e3195bfb58", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "6b1169bb-7bd1-4154-9ef8-d104c94dff04", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "752ec963-dcd1-4558-85c0-9c606f5d1181", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "10befa18-80c8-461e-bcea-5dba1d864cb0", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "c9991186-694b-4758-8e1b-ec6aa334ef4b", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "b65a8c12-f5f8-427a-9d90-eccf22d847a2", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-users", "query-groups" ] + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "0c800a73-f8f1-40ea-b91e-a6bc6e9c6b37", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "8e502dad-3945-4e58-bb7f-bdd4d2d0416d", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "18c6e46a-c5cc-45ba-9ac6-cddc2c59fd1a", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "b9171c27-55f9-4f99-9086-c1828ae0f884", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "1b5f331e-aaa5-4225-888d-b212d60211fb", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "57dbd22f-cb06-47fb-a0d3-d7bd7f715595", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "d533dda6-e2f7-4163-a0b6-4752cf74bcde", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-clients" ] + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "5342db5b-d1ba-4218-8900-3d5e2e0a8ccb", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "4bc5fc15-8488-4ef0-a0c0-4a28e0803fb4", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "bae6a63f-981f-429a-b4d4-1687a9dc5386", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "31c1c637-07a5-4884-892d-bd904f7ff1fb", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a3bb7f5a-8746-4d66-a6d3-60c355489276", + "clientId": "master-realm", + "name": "master Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" }, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - }, { - "id" : "439fe1a2-c13b-4043-832d-00558a53ec6e", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "account-console" : [ ], - "sqlpage" : [ ], - "broker" : [ ], - "master-realm" : [ ], - "account" : [ { - "id" : "f97398fd-8608-46eb-ad58-39b92dee69a7", - "name" : "view-groups", - "composite" : false, - "clientRole" : true, - "containerId" : "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", - "attributes" : { } - }, { - "id" : "722736c4-47ce-4a8f-a74a-38b37f172bc3", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", - "attributes" : { } - }, { - "id" : "9cd94925-88bd-4cd2-af60-797da71aa1e5", - "name" : "manage-account", - "composite" : false, - "clientRole" : true, - "containerId" : "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", - "attributes" : { } - } ] - } - }, - "groups" : [ ], - "defaultRole" : { - "id" : "3473c742-cfa1-4d81-975e-0d74bdf56795", - "name" : "default-roles-master", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "d7757cae-367b-4dfa-87f7-a19a789af2b9" - }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], - "localizationTexts" : { }, - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyExtraOrigins" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessExtraOrigins" : [ ], - "users" : [ { - "id" : "0cc0472e-a38b-4f45-8d91-77ecfe5c8b7d", - "username" : "demo", - "firstName" : "John", - "lastName" : "Smith", - "email" : "demo@example.com", - "emailVerified" : false, - "createdTimestamp" : 1714079479552, - "enabled" : true, - "totp" : false, - "credentials" : [ { - "id" : "d453f7cb-5ba5-45ab-a694-38ddffb93503", - "type" : "password", - "userLabel" : "My password", - "createdDate" : 1714079498525, - "secretData" : "{\"value\":\"gxi8oR/w6GPvZjUXJAsxSuxWZCDsxL3hwzjlfymoeYsRLXxJIvJdy5SeRch4BOYwNdfRwrbOenBGScCleyQkfA==\",\"salt\":\"HTsvAP/Cig6pIOVo3SPFnw==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-master" ], - "notBefore" : 0, - "groups" : [ ] - } ], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clientScopeMappings" : { - "account" : [ { - "client" : "account-console", - "roles" : [ "manage-account", "view-groups" ] - } ] - }, - "clients" : [ { - "id" : "d50163a2-d59f-4c1d-a1e5-087b5fa3920d", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/master/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/master/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "89f96859-de46-4778-bd0b-e44256ff8ea2", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "7a8f7d8b-93dc-4f5c-a8cf-2caa9802ae35", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "853110df-77fe-49b2-b8be-000ae4922b88", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "12e0887d-af61-4066-9d29-a54f2523606d", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "ff46bc2b-4c0b-49b3-9bca-0938b05a9581", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/master/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/master/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "46a75d57-9af5-4466-8e0c-deb66d6d58e8", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "12f13df0-1a10-4792-b62a-c67b3d0f9bb0", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "0c800a73-f8f1-40ea-b91e-a6bc6e9c6b37", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "8e502dad-3945-4e58-bb7f-bdd4d2d0416d", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "18c6e46a-c5cc-45ba-9ac6-cddc2c59fd1a", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "b9171c27-55f9-4f99-9086-c1828ae0f884", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "1b5f331e-aaa5-4225-888d-b212d60211fb", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "5342db5b-d1ba-4218-8900-3d5e2e0a8ccb", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "4bc5fc15-8488-4ef0-a0c0-4a28e0803fb4", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "bae6a63f-981f-429a-b4d4-1687a9dc5386", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "31c1c637-07a5-4884-892d-bd904f7ff1fb", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "a3bb7f5a-8746-4d66-a6d3-60c355489276", - "clientId" : "master-realm", - "name" : "master Realm", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "6e805d7f-9d7f-4f26-91e7-c58ea91df054", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "ccd6d83f-2da8-40dd-b94d-77ded30dc831", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "119d7e06-8651-42b6-af42-4ea02bfb49e0", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "7dd81fc3-b99a-4f3a-8a5a-ab072e8bf935", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "d97602ca-263b-4115-88d8-fa1470e231db", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "1d5c2948-1c22-4444-9de2-ec9d5fab24a2", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "64760efc-d03f-4f2f-8cae-63684200eace", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "52dd9974-2d5c-47ef-8229-d1c97e59d4f0", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ ], - "optionalClientScopes" : [ ] - }, { - "id" : "12a6effc-cf82-4dff-9bdc-d7610b86d89c", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/master/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/master/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "baecafba-25d9-473e-a7af-72d18a84fd83", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "a2bec2b8-f850-405e-9f26-59063ffa6f08", - "clientId" : "sqlpage", - "name" : "SQLPage Example App", - "description" : "", - "rootUrl" : "http://localhost:8080/", - "adminUrl" : "http://localhost:8080/", - "baseUrl" : "", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : true, - "clientAuthenticatorType" : "client-secret", - "secret" : "qiawfnYrYzsmoaOZT28rRjPPRamfvrYr", - "redirectUris" : [ "http://localhost:8080/oidc_redirect_handler.sql" ], - "webOrigins" : [ "http://localhost:8080" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : true, - "protocol" : "openid-connect", - "attributes" : { - "client.secret.creation.time" : "1714080951", - "post.logout.redirect.uris" : "+##http://localhost:8080/", - "oauth2.device.authorization.grant.enabled" : "false", - "backchannel.logout.revoke.offline.tokens" : "false", - "use.refresh.tokens" : "true", - "oidc.ciba.grant.enabled" : "false", - "client.use.lightweight.access.token.enabled" : "false", - "backchannel.logout.session.required" : "true", - "client_credentials.use_refresh_token" : "false", - "tls.client.certificate.bound.access.tokens" : "false", - "require.pushed.authorization.requests" : "false", - "acr.loa.map" : "{}", - "display.on.consent.screen" : "false", - "token.response.type.bearer.lower-case" : "false" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : -1, - "protocolMappers" : [ { - "id" : "62a6fb7a-c7e5-44c3-b878-5bbfa932dc3c", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - }, { - "id" : "9b15bf7e-28c9-46dd-8e38-0d69f8cab137", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "15150602-9a7d-4850-8f69-17b3553174c4", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "05e051d1-3954-43c4-a4ea-73b5879dac17", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "4b0aaf08-2b73-4d18-8030-83a20b560e62", - "clientId" : "sqlpage_cas_demo", - "name" : "SQLPage CAS demo", - "description" : "This is using keycloak, but hopefully this is compatible with Apero CAS.", - "rootUrl" : "http://localhost:8080/", - "adminUrl" : "http://localhost:8080/", - "baseUrl" : "http://localhost:8080/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "http://localhost:8080/cas/redirect_handler.sql" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : true, - "protocol" : "cas", - "attributes" : { - "post.logout.redirect.uris" : "http://localhost:8080/cas/redirect_handler.sql" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : -1, - "protocolMappers" : [ { - "id" : "afe008e7-a775-45ca-9b88-7d921a816aad", - "name" : "family name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "lastName", - "claim.name" : "sn", - "jsonType.label" : "String" - } - }, { - "id" : "e54f5acb-5ed1-4640-99ed-03fc3f92c520", - "name" : "given name", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "firstName", - "claim.name" : "givenName", - "jsonType.label" : "String" - } - }, { - "id" : "af22294a-2ba1-4085-aee7-ccebab888ec4", - "name" : "full name", - "protocol" : "cas", - "protocolMapper" : "cas-full-name-mapper", - "consentRequired" : false, - "config" : { - "claim.name" : "cn", - "jsonType.label" : "String" - } - }, { - "id" : "569ec0d9-c656-4492-b3a2-0a8d1a4df029", - "name" : "email", - "protocol" : "cas", - "protocolMapper" : "cas-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "email", - "claim.name" : "mail", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ ], - "optionalClientScopes" : [ ] - } ], - "clientScopes" : [ { - "id" : "c14c09ff-087e-4272-9cc4-b2a997f64a55", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${addressScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "60c43d69-2518-4424-a5b8-5cdc33e2ac17", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "introspection.token.claim" : "true", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - }, { - "id" : "03ea4147-a506-45a9-84ae-e1efe2708eea", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${emailScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "04d4097a-16b3-4155-9ff3-672d04079e16", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - }, { - "id" : "f3bdae59-35e2-46b8-9625-a53a066993b2", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - } ] - }, { - "id" : "7bbedae3-a3b5-4d76-b457-b00010254408", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${profileScopeConsentText}" - }, - "protocolMappers" : [ { - "id" : "7c97d390-3a24-4013-85a7-674427d49ab8", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - }, { - "id" : "b740d3b8-38c7-460e-94aa-6525aec2e00b", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "df3c39d5-cd3f-4d38-899c-d0899d82dcbb", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - }, { - "id" : "9dbeb8fe-333d-4311-a389-42075ff057b4", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "19e29316-24dd-4e4a-aa85-e767cd867a79", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - }, { - "id" : "2bfa792d-ea47-4d43-b456-11cde3c937f2", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "c4aa41a4-bef1-401b-abe8-3d48f101020d", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "c9617211-8247-4b5e-9b71-58e61bdaab21", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - }, { - "id" : "b7ae04f4-415e-4e0e-b7b6-1af6841d22bb", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "51a166d3-ba9a-4ba6-b1a1-d259563aa49a", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "e3c90b13-0db0-40f7-b640-487f18b01214", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "cd5cfea9-9151-486c-9ec1-e9d503543201", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "bfc74493-3b09-4dac-b7be-41cc6d2e209e", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "8dfd3646-a624-43ee-85de-82aa171f3ad7", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "990085f5-6624-43e0-bd8e-ee19427c9900", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "824634cf-dfc0-4fc9-b47b-3803a5868e6f", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "6e805d7f-9d7f-4f26-91e7-c58ea91df054", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "ccd6d83f-2da8-40dd-b94d-77ded30dc831", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "119d7e06-8651-42b6-af42-4ea02bfb49e0", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "7dd81fc3-b99a-4f3a-8a5a-ab072e8bf935", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5cfd9edd-a0a5-4ead-ac29-0a4b1ffa17db", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "d97602ca-263b-4115-88d8-fa1470e231db", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "1d5c2948-1c22-4444-9de2-ec9d5fab24a2", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "64760efc-d03f-4f2f-8cae-63684200eace", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "52dd9974-2d5c-47ef-8229-d1c97e59d4f0", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [], + "optionalClientScopes": [] + }, + { + "id": "12a6effc-cf82-4dff-9bdc-d7610b86d89c", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/master/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/admin/master/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "baecafba-25d9-473e-a7af-72d18a84fd83", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "a2bec2b8-f850-405e-9f26-59063ffa6f08", + "clientId": "sqlpage", + "name": "SQLPage SSO Demo App", + "description": "", + "rootUrl": "http://localhost:8080/", + "adminUrl": "http://localhost:8080/", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": true, + "clientAuthenticatorType": "client-secret", + "secret": "qiawfnYrYzsmoaOZT28rRjPPRamfvrYr", + "redirectUris": ["http://localhost:8080/sqlpage/oidc_callback"], + "webOrigins": ["http://localhost:8080"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "client.secret.creation.time": "1714080951", + "login_theme": "keycloak", + "post.logout.redirect.uris": "+##http://localhost:8080/", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "client.use.lightweight.access.token.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "tls.client.certificate.bound.access.tokens": "false", + "require.pushed.authorization.requests": "false", + "acr.loa.map": "{}", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "62a6fb7a-c7e5-44c3-b878-5bbfa932dc3c", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + }, + { + "id": "9b15bf7e-28c9-46dd-8e38-0d69f8cab137", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "15150602-9a7d-4850-8f69-17b3553174c4", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "05e051d1-3954-43c4-a4ea-73b5879dac17", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "4b0aaf08-2b73-4d18-8030-83a20b560e62", + "clientId": "sqlpage_cas_demo", + "name": "SQLPage CAS demo", + "description": "This is using keycloak, but hopefully this is compatible with Apero CAS.", + "rootUrl": "http://localhost:8080/", + "adminUrl": "http://localhost:8080/", + "baseUrl": "http://localhost:8080/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["http://localhost:8080/cas/redirect_handler.sql"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "cas", + "attributes": { + "post.logout.redirect.uris": "http://localhost:8080/cas/redirect_handler.sql" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "afe008e7-a775-45ca-9b88-7d921a816aad", + "name": "family name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "sn", + "jsonType.label": "String" + } + }, + { + "id": "e54f5acb-5ed1-4640-99ed-03fc3f92c520", + "name": "given name", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "givenName", + "jsonType.label": "String" + } + }, + { + "id": "af22294a-2ba1-4085-aee7-ccebab888ec4", + "name": "full name", + "protocol": "cas", + "protocolMapper": "cas-full-name-mapper", + "consentRequired": false, + "config": { + "claim.name": "cn", + "jsonType.label": "String" + } + }, + { + "id": "569ec0d9-c656-4492-b3a2-0a8d1a4df029", + "name": "email", + "protocol": "cas", + "protocolMapper": "cas-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "mail", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [], + "optionalClientScopes": [] } - }, { - "id" : "5b5a7a38-9746-4593-b94d-4dbcb79a57d8", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" + ], + "clientScopes": [ + { + "id": "c14c09ff-087e-4272-9cc4-b2a997f64a55", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "60c43d69-2518-4424-a5b8-5cdc33e2ac17", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "03ea4147-a506-45a9-84ae-e1efe2708eea", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "04d4097a-16b3-4155-9ff3-672d04079e16", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "f3bdae59-35e2-46b8-9625-a53a066993b2", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "7bbedae3-a3b5-4d76-b457-b00010254408", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "7c97d390-3a24-4013-85a7-674427d49ab8", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "b740d3b8-38c7-460e-94aa-6525aec2e00b", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "df3c39d5-cd3f-4d38-899c-d0899d82dcbb", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "9dbeb8fe-333d-4311-a389-42075ff057b4", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "19e29316-24dd-4e4a-aa85-e767cd867a79", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "2bfa792d-ea47-4d43-b456-11cde3c937f2", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "c4aa41a4-bef1-401b-abe8-3d48f101020d", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "c9617211-8247-4b5e-9b71-58e61bdaab21", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "b7ae04f4-415e-4e0e-b7b6-1af6841d22bb", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "51a166d3-ba9a-4ba6-b1a1-d259563aa49a", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e3c90b13-0db0-40f7-b640-487f18b01214", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "cd5cfea9-9151-486c-9ec1-e9d503543201", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "bfc74493-3b09-4dac-b7be-41cc6d2e209e", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "8dfd3646-a624-43ee-85de-82aa171f3ad7", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "990085f5-6624-43e0-bd8e-ee19427c9900", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "824634cf-dfc0-4fc9-b47b-3803a5868e6f", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "5b5a7a38-9746-4593-b94d-4dbcb79a57d8", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "34321bc7-3fee-4999-b718-19d907b221a4", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "2a7bc0fb-adce-4b3d-b423-7035f31fb5a9", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "bd166300-5b05-4726-b0eb-68c29e35f1af", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ef839796-9f9a-47c0-9685-c9edcf6754a5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "f74a2e81-da33-4e8d-943a-d5e665adf499", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "d5196e6b-5bb9-4eb0-92b3-2f8d129d5802", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "cdf64576-9f80-482a-bcff-6c547309d9bd", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "6c3ffd12-fa43-4cb8-a118-c4643b98df70", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "c2045a80-7b4d-4e06-acb9-7299ba16134c", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "ad467869-6344-40b2-af7a-70687d76e6fd", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "7c7710a3-e3a7-4c70-a246-04a14c9733bb", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "780cb5bb-a37d-4f87-a7d5-a0942a2c1589", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "3f65310d-21f0-4973-8c67-457a0d851ba3", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + }, + { + "id": "1a6720eb-c33d-4c39-9d42-051aa1cf7d85", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] } - } ] - }, { - "id" : "34321bc7-3fee-4999-b718-19d907b221a4", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, - "protocolMappers" : [ { - "id" : "2a7bc0fb-adce-4b3d-b423-7035f31fb5a9", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" - } - } ] - }, { - "id" : "bd166300-5b05-4726-b0eb-68c29e35f1af", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${phoneScopeConsentText}" + "smtpServer": {}, + "loginTheme": "keycloak", + "accountTheme": "keycloak.v3", + "adminTheme": "keycloak.v2", + "emailTheme": "keycloak", + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "e89fe502-91fd-4f04-bdcc-4b14e817e4dc", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "1414070f-5cd6-4446-b53a-dcd9c28c26a5", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "1e6d1ac0-d10b-4eaf-8b87-39ccf6a755d2", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper" + ] + } + }, + { + "id": "b34cb2f6-806e-49c3-822a-614b4465faf7", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "81af21c3-da8a-4f70-9b33-43d0150f0bbb", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "58f683ad-378d-4f9f-93db-630007bef02a", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + }, + { + "id": "26a3c97d-052b-40b6-8d8f-8950befb1837", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "1a2b238f-2b2c-488f-977e-955c62134b1a", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "a49f2d51-c152-468d-a070-28a931fab95c", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "74f24861-5376-4224-bdc0-1d511b60e8df", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEAvYkJyhtq3Pi3GjppeBXN8QLvNd6LNM/78d/3XJCZPNizPfCEQk5IDJlxJaqEIsoYZaB0f2Hr3/SBTVvYnSPQhr2hY/FKnk4ptkgieJf6Sc6rUO/2w/zITePexTjIdmbOD9ftS3FxLoiNq/kJ8ZeBpx/Jdpusu20CyrCIVmDS57O0lumXMrwenyu8bF2rx9MNwDlUzFcKENaQDcWnxmD9xPOue+MvtaAqG/BRf1/QRiu5CT1IPh1FjevCmiUOlpe4CUuZ2vRhb6SXKdE+Ha/HvbvjSuloZ9bFOL1LcOPRDY6wlHFiWNJ3Zs+Es0jMJ3dT7+/rAh4Z0pPQLBAhzryFqwIDAQABAoIBAAOzyRQAbBp2kJu7t11dTlaztXjVE4gLu1f9WtIdOvkOzJWG/LZk3BBu8OBeT9LJetIwC7EvTacOxMso4kwo1m/DWtiJGT7gDP2JXtmsh6WTGr1BKrinrUFO6bDq4TOQN4c6CGHDDLBu12xGuD8sCUrP7/oCpCGhyVOslhqF1/4m97IlYjNPLQ5PPTIg9Am1zDOl/psXxgu+zWzUlI89ARGDNsrwCbcd3qFrxKCidoNkXjbaNa/YsMSbOdSNLbthjvpR78qAoGBC0vYwaR/4c+jaAc2E29gahEn0DOKGEKKFqnprj7BRIiiXasEQX+A6Wfs6v/pPdcQlklLFFncZKF0CgYEA7UMx+/CaGZMSc/AVPnbRO6FQ1d/jjZEXI3tflz9cELM2gyqMMEIe9/uxRCu/RKH73t+V07z65iGRVdDWNMZ1iIQpt50hblicJvF83YT2tjxQExnQo7sle9Jm9wApTfIOb4bolrdtSc7hEYYYiTUIgPBA2tuj9dOQt91kLnN1r+cCgYEAzIDtw3Kmurn72kJ168z1CkHuWBx4DtfEQY0IGUu/X68x/jF6VE8ouXvJuWx49FLJtf1xFnBT4HJ0Il+4UFGomKn6Xd7z7635KZfgQ1L7rwYv7GqOlI4XS4FmMYezyAOtYaosOBLzB/yabNCCGKHMRSrulHzYUCK9aPY7/B5Ck50CgYEAlqeKP53BW+flWbTi6Gzt4t1FxOiLR0MP3DnkstdKkFgbjyIfLi1uGKy7HLxikSQCGL0EGBTxg9tgu4sF2TEDRJIXIz4lEjo1vQyt6sMZHRIjDl3f+3dED+HD+6cgkxvWSr7xRXJndOxmQYhSYB1KrwTfSZkZ/Wg/hmCP0mcCHZUCgYEAhuvQ8g/kXHFz7g3HCulQCZJyE4PE2dYUz0Kiwz2sZw6JJzGxiYooTieTcVhVfKxaFE2/nJRDYmNgp4ULb0JQv1f1rJT5z3myV3SyKvjGwDSOzaWHqA8O42vd5nOncyCp9TN2tRAbc3t+zqfKDUJCKKgoe6LafBRPbr512OKF/ikCgYAZrlAVoN3jzwZxdszFzyz5czDtv2MMepNoYvNsSBNo0tfo8qThByTcN6S5u3q5yMuAMPyhajJ/UYtPxKK75bLkjobkxeX9MjSzwCK6s8DNCwqJJlXFjOsNOigTuB9ku0xq9P5D7AGD1rOgaUE72NjBZLS/c2jwX3Xv2mE2qWpViQ==" + ], + "certificate": [ + "MIICpzCCAY8CBgGPFxVXxDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wHhcNMjQwNDI1MjEwNTI1WhcNMzQwNDI1MjEwNzA1WjAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9iQnKG2rc+LcaOml4Fc3xAu813os0z/vx3/dckJk82LM98IRCTkgMmXElqoQiyhhloHR/Yevf9IFNW9idI9CGvaFj8UqeTim2SCJ4l/pJzqtQ7/bD/MhN497FOMh2Zs4P1+1LcXEuiI2r+Qnxl4GnH8l2m6y7bQLKsIhWYNLns7SW6ZcyvB6fK7xsXavH0w3AOVTMVwoQ1pANxafGYP3E86574y+1oCob8FF/X9BGK7kJPUg+HUWN68KaJQ6Wl7gJS5na9GFvpJcp0T4dr8e9u+NK6Whn1sU4vUtw49ENjrCUcWJY0ndmz4SzSMwnd1Pv7+sCHhnSk9AsECHOvIWrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHVqRlWO/uDsyIztqQjJyYbk3TtA1JaCeCOUhHN+7MD78/KGHQwAMU8dTuU/3YajVwm+0uMbnZ4gpV/mUkZqt9aVsiDk2GJ6P4z4H5Tg8BGSLYCVYEuKU7lSXNePxfrHsYf0jC209KJmGPynPwVYmpqZ92ucC0GGQyFRARyjhpx0pg7yxy3ARbzhYT2uggtzdv9DdkZ6vnm6siPlQb4VjDZB76XueT6b8/qNeDVzjfh1igBTyavY3UES9l2bdpQAjUHc6JZVP2xQEAEioHpYv3opAOo6Egu90ON7DeQupukSQEqizvhS9LbVsGKg0iKiVFFazdNtYvamOG1+d4slhoU=" + ], + "priority": ["100"], + "algorithm": ["RSA-OAEP"] + } + }, + { + "id": "814f5c3e-a455-4b04-a90f-4752382b106e", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": ["17498c29-d8ba-4851-bd66-3b56739fbe42"], + "secret": ["Qo8khARW9YKp0Zdh7rLE1g"], + "priority": ["100"] + } + }, + { + "id": "6685a900-2ea7-49ec-b5b9-bd8717784f0d", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEogIBAAKCAQEAuP4foFrftmCEJmZ740D+Top9ezC7gnFVGmXxMbPFmGb8HAqRab2pTKy5CRv6e9DIGyrkEKfbkLM8Vj1XgWg/+7YEovDc08v7biHyHX1LlorVTcZQ6w7fXYdq63iIACV2B1HqhOgd9kGpkwplJRYSGG1+dg1+YySxghiziS3MIfGuwL7sTTAsOePrLj2Y6/MIUDqlu2bXcGXj8CB7ScWy/gdhOu8fAPLKniGIOhQno7me5tbj4s34VoqBy1t9CuWexEi+NIrPJdeum5EDPJ0x/BCGzlHBoA1Iq7MhmhKeFFARmQ+ubPfHdUfvuyswj4fKD2w37cZT8PtUgd3eMssQXQIDAQABAoIBACfA+IPps2SKViu4X0wlReET8sY74Te1ah/ro0rWgpJvIyNVhA0wpEalYXgbKpdb9PydmXgY0l7EnaU8tmbJQ+KwKUvordPX5Ha01cZPjCRUPmVhxjbVMdv0A16JvtQlOLl2+YpJJVMrpijClZzEIuxb706oNK5SjtDRxRcoH9N1MW+d4KNdJzuyHAfFBhFtydx02Uxjg5ptkwCCNwZGqdMtlp4/VGQGGRQEiZCnuOr9JX0U2o0slcCdc4OnJSs1Sf8QVHuefqM3Kf34WEDJ0jScDKV0EMKjGNBdMLLEXBpI9V9sKKvR72Bx5nP/j8/pFN7WOmSwYhwHiDkLS3Gk8hMCgYEA+mzM8W1MZBwToaHAOwMHvc/8lMDBztbfN6/mJrxlDal8v+f932C4ifvKZNmSCBtwFMJxIAcbcxBcJuX4Dj397Ch6ocXvs7kTOxdzppyWc4oI7uaEhGGcLHVQY27qkx4RLCS0ptipWdHTz/51Uj1dVgRtg3KEmIHInqe/4UZKCb8CgYEAvRxqvs2kwAoVxBM4CFBVFjNNnxgPFtVmHAfbKZTPAej87sfLzQCj4QBS8mK603A4J0fh6eJ08kL4FZovnyVGV9oevFIMvOPXZbU953qktGwCIrK9AxmiqZP4sgI8/plEh0VgMwhUg5uh4Mp/ZP9Ag/MXCkxKal19ppmRuRr+lOMCgYBtRsjvmRg6nx3Z7DFsDthz9axsZOitj4n8TN+Li641Ff5/54Ya0aP1YlBhTaexrfdst6SRq0hJH5x2xOdHn7mMMeXBbhQ5Qsunf4ZR8AafCF75kNHGyqlRpSedHCt0YyxvLN0/6U+NCEj7fDhJ2Mk/3dLEB1bhDdEzmlPaw8dPFQKBgEBgeh42F02goUQ8XqjF4BFMqbHtGMXnI3mLWxpOpCG8VM5ciY5iF2ezGomU/pCX9SW6HLfn9XO7RITmFiwRHl8ty6TEMb3juiHPjyFL6OHamueA/UMe6Pbdfp3qkSUCvAdooJT+0vZydqr1hGS3WBkTGdbRncuTxACA6tCe1eeNAoGABeQZWBOW/EvUx7k3lEYr51xWK+R8Lk3xibvg1oiNKtmj+9A+p9Adt5l8z6Q1vwnV10qPbDIy0ja8wkcX7mqCMDZf7A5yEQtvQFNbbk64iwLSPx2H207vn6lVimiSskAUZ1eVFMTxzIddaqmOxzEng6y6gfBOVK0xksgLNnV+NKw=" + ], + "certificate": [ + "MIICpzCCAY8CBgGPFxVYJTANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wHhcNMjQwNDI1MjEwNTI1WhcNMzQwNDI1MjEwNzA1WjAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4/h+gWt+2YIQmZnvjQP5Oin17MLuCcVUaZfExs8WYZvwcCpFpvalMrLkJG/p70MgbKuQQp9uQszxWPVeBaD/7tgSi8NzTy/tuIfIdfUuWitVNxlDrDt9dh2rreIgAJXYHUeqE6B32QamTCmUlFhIYbX52DX5jJLGCGLOJLcwh8a7AvuxNMCw54+suPZjr8whQOqW7ZtdwZePwIHtJxbL+B2E67x8A8sqeIYg6FCejuZ7m1uPizfhWioHLW30K5Z7ESL40is8l166bkQM8nTH8EIbOUcGgDUirsyGaEp4UUBGZD65s98d1R++7KzCPh8oPbDftxlPw+1SB3d4yyxBdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALKVXfayldWsdDjwtUZOtzu9fTU3YbUekvkuYr0Fvs9348ZiWoPvt6JQ9i7ytqDxok9CvgVL347ZS+lDkMKhBpw7ryVS0bG/Vg7DeNmjmutGuGdiJR2nUF8z6SgDVWXqr4XrcDy6xwfVtAazc8MXau+eQozlZBiLV4bKDD793m9zqPeSIIipDozMrfKm4jYnam33d9pRQFGDgEGHqXiwR96x8tC5zlFjngKlX1IgigYqARSsOMaV4vU2aIhIq3bLpvSIGGSDo9iw6iYhBYn9tpmtsHCU/RFqsWPhglU168+0VQesCQphKCXoOZp3qIGnRUVySNZSZrHynQ/wLzI1Dos=" + ], + "priority": ["100"] + } + }, + { + "id": "2b7d2bdf-aee4-4f31-8bec-97bd01182220", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": ["f908ef09-5b02-4d60-8aa9-906f5be4b4b3"], + "secret": [ + "_PZq7evS8vCHPBBLIRlgHrcXtE46TgjSaao5Yh1LlyjHUyHhxarMYYbenDFELpc7nw3WDWr2U0lS-y0QY7EHySYvf6zx5er1hNwPV78g4kvJUYRKKf9U8OmWlsr2E8bDGKBr547El5HyU11_KWykzvi_dBkqX6LsceQB8guy8t4" + ], + "priority": ["100"], + "algorithm": ["HS512"] + } + } + ] }, - "protocolMappers" : [ { - "id" : "ef839796-9f9a-47c0-9685-c9edcf6754a5", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "68f8d935-47fb-42dc-8e23-94cca509c6b1", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "c5c5650b-a462-4b37-9dd9-f1499e369219", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "16124aaf-0384-4d6d-a108-338b5749aa5d", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "3bbf73c5-00f6-4a2a-a2cb-d74f82653fa3", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b0e4e594-f55d-46be-86fd-676ec72d979e", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "04618969-a055-4e6f-9e41-27c24b037d30", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "079f9946-bc86-4b50-970f-0fde0eebafb0", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "b4056290-6f49-4343-a803-8029b8cab587", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "1b02c9b7-50d4-4001-8648-4674d2b5c83f", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "2c52b4db-2764-4930-ba72-eed08a3568f9", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d4b0242b-f4a2-4e28-9dc5-5ee8bd64c4a7", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "5c90d095-2a79-40c5-a452-98e0cd9402e6", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "87054128-dec5-47c4-a461-0491ac601ee8", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "197b6d93-54f1-46a1-8bc1-fee099e06978", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "ca5c0b1a-51cf-4a79-bb61-5b923eed9bf7", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "856a26b0-cf52-4173-94fb-8726c77de2ab", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ea7cafde-a77b-4fd6-9510-396748960980", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "35a9e22a-3775-4cad-9a8b-d646cd74c8eb", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] } - }, { - "id" : "f74a2e81-da33-4e8d-943a-d5e665adf499", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" + ], + "authenticatorConfig": [ + { + "id": "4a307cbf-79bc-4125-8eca-4ea98f7cb27b", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "0a0aaedd-39f6-4dc0-9153-ac6dbd255d3a", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } } - } ] - }, { - "id" : "d5196e6b-5bb9-4eb0-92b3-2f8d129d5802", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false", - "consent.screen.text" : "" - }, - "protocolMappers" : [ { - "id" : "cdf64576-9f80-482a-bcff-6c547309d9bd", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} } - } ] - }, { - "id" : "6c3ffd12-fa43-4cb8-a118-c4643b98df70", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "c2045a80-7b4d-4e06-acb9-7299ba16134c", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "frontendUrl": "http://localhost:8181", + "acr.loa.map": "{}" }, - "protocolMappers" : [ { - "id" : "ad467869-6344-40b2-af7a-70687d76e6fd", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - } ] - }, { - "id" : "7c7710a3-e3a7-4c70-a246-04a14c9733bb", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "true", - "consent.screen.text" : "${rolesScopeConsentText}" + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] }, - "protocolMappers" : [ { - "id" : "780cb5bb-a37d-4f87-a7d5-a0942a2c1589", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String" - } - }, { - "id" : "3f65310d-21f0-4973-8c67-457a0d851ba3", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String" - } - }, { - "id" : "1a6720eb-c33d-4c39-9d42-051aa1cf7d85", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection" : "1; mode=block", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "loginTheme" : "keycloak", - "accountTheme" : "", - "adminTheme" : "", - "emailTheme" : "", - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "components" : { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { - "id" : "e89fe502-91fd-4f04-bdcc-4b14e817e4dc", - "name" : "Full Scope Disabled", - "providerId" : "scope", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "1414070f-5cd6-4446-b53a-dcd9c28c26a5", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "1e6d1ac0-d10b-4eaf-8b87-39ccf6a755d2", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-full-name-mapper" ] - } - }, { - "id" : "b34cb2f6-806e-49c3-822a-614b4465faf7", - "name" : "Consent Required", - "providerId" : "consent-required", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "81af21c3-da8a-4f70-9b33-43d0150f0bbb", - "name" : "Max Clients Limit", - "providerId" : "max-clients", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "max-clients" : [ "200" ] - } - }, { - "id" : "58f683ad-378d-4f9f-93db-630007bef02a", - "name" : "Trusted Hosts", - "providerId" : "trusted-hosts", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "host-sending-registration-request-must-match" : [ "true" ], - "client-uris-must-match" : [ "true" ] - } - }, { - "id" : "26a3c97d-052b-40b6-8d8f-8950befb1837", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper" ] - } - }, { - "id" : "1a2b238f-2b2c-488f-977e-955c62134b1a", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - } ], - "org.keycloak.userprofile.UserProfileProvider" : [ { - "id" : "a49f2d51-c152-468d-a070-28a931fab95c", - "providerId" : "declarative-user-profile", - "subComponents" : { }, - "config" : { - "kc.user.profile.config" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" ] - } - } ], - "org.keycloak.keys.KeyProvider" : [ { - "id" : "74f24861-5376-4224-bdc0-1d511b60e8df", - "name" : "rsa-enc-generated", - "providerId" : "rsa-enc-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEpAIBAAKCAQEAvYkJyhtq3Pi3GjppeBXN8QLvNd6LNM/78d/3XJCZPNizPfCEQk5IDJlxJaqEIsoYZaB0f2Hr3/SBTVvYnSPQhr2hY/FKnk4ptkgieJf6Sc6rUO/2w/zITePexTjIdmbOD9ftS3FxLoiNq/kJ8ZeBpx/Jdpusu20CyrCIVmDS57O0lumXMrwenyu8bF2rx9MNwDlUzFcKENaQDcWnxmD9xPOue+MvtaAqG/BRf1/QRiu5CT1IPh1FjevCmiUOlpe4CUuZ2vRhb6SXKdE+Ha/HvbvjSuloZ9bFOL1LcOPRDY6wlHFiWNJ3Zs+Es0jMJ3dT7+/rAh4Z0pPQLBAhzryFqwIDAQABAoIBAAOzyRQAbBp2kJu7t11dTlaztXjVE4gLu1f9WtIdOvkOzJWG/LZk3BBu8OBeT9LJetIwC7EvTacOxMso4kwo1m/DWtiJGT7gDP2JXtmsh6WTGr1BKrinrUFO6bDq4TOQN4c6CGHDDLBu12xGuD8sCUrP7/oCpCGhyVOslhqF1/4m97IlYjNPLQ5PPTIg9Am1zDOl/psXxgu+zWzUlI89ARGDNsrwCbcd3qFrxKCidoNkXjbaNa/YsMSbOdSNLbthjvpR78qAoGBC0vYwaR/4c+jaAc2E29gahEn0DOKGEKKFqnprj7BRIiiXasEQX+A6Wfs6v/pPdcQlklLFFncZKF0CgYEA7UMx+/CaGZMSc/AVPnbRO6FQ1d/jjZEXI3tflz9cELM2gyqMMEIe9/uxRCu/RKH73t+V07z65iGRVdDWNMZ1iIQpt50hblicJvF83YT2tjxQExnQo7sle9Jm9wApTfIOb4bolrdtSc7hEYYYiTUIgPBA2tuj9dOQt91kLnN1r+cCgYEAzIDtw3Kmurn72kJ168z1CkHuWBx4DtfEQY0IGUu/X68x/jF6VE8ouXvJuWx49FLJtf1xFnBT4HJ0Il+4UFGomKn6Xd7z7635KZfgQ1L7rwYv7GqOlI4XS4FmMYezyAOtYaosOBLzB/yabNCCGKHMRSrulHzYUCK9aPY7/B5Ck50CgYEAlqeKP53BW+flWbTi6Gzt4t1FxOiLR0MP3DnkstdKkFgbjyIfLi1uGKy7HLxikSQCGL0EGBTxg9tgu4sF2TEDRJIXIz4lEjo1vQyt6sMZHRIjDl3f+3dED+HD+6cgkxvWSr7xRXJndOxmQYhSYB1KrwTfSZkZ/Wg/hmCP0mcCHZUCgYEAhuvQ8g/kXHFz7g3HCulQCZJyE4PE2dYUz0Kiwz2sZw6JJzGxiYooTieTcVhVfKxaFE2/nJRDYmNgp4ULb0JQv1f1rJT5z3myV3SyKvjGwDSOzaWHqA8O42vd5nOncyCp9TN2tRAbc3t+zqfKDUJCKKgoe6LafBRPbr512OKF/ikCgYAZrlAVoN3jzwZxdszFzyz5czDtv2MMepNoYvNsSBNo0tfo8qThByTcN6S5u3q5yMuAMPyhajJ/UYtPxKK75bLkjobkxeX9MjSzwCK6s8DNCwqJJlXFjOsNOigTuB9ku0xq9P5D7AGD1rOgaUE72NjBZLS/c2jwX3Xv2mE2qWpViQ==" ], - "certificate" : [ "MIICpzCCAY8CBgGPFxVXxDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wHhcNMjQwNDI1MjEwNTI1WhcNMzQwNDI1MjEwNzA1WjAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9iQnKG2rc+LcaOml4Fc3xAu813os0z/vx3/dckJk82LM98IRCTkgMmXElqoQiyhhloHR/Yevf9IFNW9idI9CGvaFj8UqeTim2SCJ4l/pJzqtQ7/bD/MhN497FOMh2Zs4P1+1LcXEuiI2r+Qnxl4GnH8l2m6y7bQLKsIhWYNLns7SW6ZcyvB6fK7xsXavH0w3AOVTMVwoQ1pANxafGYP3E86574y+1oCob8FF/X9BGK7kJPUg+HUWN68KaJQ6Wl7gJS5na9GFvpJcp0T4dr8e9u+NK6Whn1sU4vUtw49ENjrCUcWJY0ndmz4SzSMwnd1Pv7+sCHhnSk9AsECHOvIWrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHVqRlWO/uDsyIztqQjJyYbk3TtA1JaCeCOUhHN+7MD78/KGHQwAMU8dTuU/3YajVwm+0uMbnZ4gpV/mUkZqt9aVsiDk2GJ6P4z4H5Tg8BGSLYCVYEuKU7lSXNePxfrHsYf0jC209KJmGPynPwVYmpqZ92ucC0GGQyFRARyjhpx0pg7yxy3ARbzhYT2uggtzdv9DdkZ6vnm6siPlQb4VjDZB76XueT6b8/qNeDVzjfh1igBTyavY3UES9l2bdpQAjUHc6JZVP2xQEAEioHpYv3opAOo6Egu90ON7DeQupukSQEqizvhS9LbVsGKg0iKiVFFazdNtYvamOG1+d4slhoU=" ], - "priority" : [ "100" ], - "algorithm" : [ "RSA-OAEP" ] - } - }, { - "id" : "814f5c3e-a455-4b04-a90f-4752382b106e", - "name" : "aes-generated", - "providerId" : "aes-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "17498c29-d8ba-4851-bd66-3b56739fbe42" ], - "secret" : [ "Qo8khARW9YKp0Zdh7rLE1g" ], - "priority" : [ "100" ] - } - }, { - "id" : "6685a900-2ea7-49ec-b5b9-bd8717784f0d", - "name" : "rsa-generated", - "providerId" : "rsa-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEogIBAAKCAQEAuP4foFrftmCEJmZ740D+Top9ezC7gnFVGmXxMbPFmGb8HAqRab2pTKy5CRv6e9DIGyrkEKfbkLM8Vj1XgWg/+7YEovDc08v7biHyHX1LlorVTcZQ6w7fXYdq63iIACV2B1HqhOgd9kGpkwplJRYSGG1+dg1+YySxghiziS3MIfGuwL7sTTAsOePrLj2Y6/MIUDqlu2bXcGXj8CB7ScWy/gdhOu8fAPLKniGIOhQno7me5tbj4s34VoqBy1t9CuWexEi+NIrPJdeum5EDPJ0x/BCGzlHBoA1Iq7MhmhKeFFARmQ+ubPfHdUfvuyswj4fKD2w37cZT8PtUgd3eMssQXQIDAQABAoIBACfA+IPps2SKViu4X0wlReET8sY74Te1ah/ro0rWgpJvIyNVhA0wpEalYXgbKpdb9PydmXgY0l7EnaU8tmbJQ+KwKUvordPX5Ha01cZPjCRUPmVhxjbVMdv0A16JvtQlOLl2+YpJJVMrpijClZzEIuxb706oNK5SjtDRxRcoH9N1MW+d4KNdJzuyHAfFBhFtydx02Uxjg5ptkwCCNwZGqdMtlp4/VGQGGRQEiZCnuOr9JX0U2o0slcCdc4OnJSs1Sf8QVHuefqM3Kf34WEDJ0jScDKV0EMKjGNBdMLLEXBpI9V9sKKvR72Bx5nP/j8/pFN7WOmSwYhwHiDkLS3Gk8hMCgYEA+mzM8W1MZBwToaHAOwMHvc/8lMDBztbfN6/mJrxlDal8v+f932C4ifvKZNmSCBtwFMJxIAcbcxBcJuX4Dj397Ch6ocXvs7kTOxdzppyWc4oI7uaEhGGcLHVQY27qkx4RLCS0ptipWdHTz/51Uj1dVgRtg3KEmIHInqe/4UZKCb8CgYEAvRxqvs2kwAoVxBM4CFBVFjNNnxgPFtVmHAfbKZTPAej87sfLzQCj4QBS8mK603A4J0fh6eJ08kL4FZovnyVGV9oevFIMvOPXZbU953qktGwCIrK9AxmiqZP4sgI8/plEh0VgMwhUg5uh4Mp/ZP9Ag/MXCkxKal19ppmRuRr+lOMCgYBtRsjvmRg6nx3Z7DFsDthz9axsZOitj4n8TN+Li641Ff5/54Ya0aP1YlBhTaexrfdst6SRq0hJH5x2xOdHn7mMMeXBbhQ5Qsunf4ZR8AafCF75kNHGyqlRpSedHCt0YyxvLN0/6U+NCEj7fDhJ2Mk/3dLEB1bhDdEzmlPaw8dPFQKBgEBgeh42F02goUQ8XqjF4BFMqbHtGMXnI3mLWxpOpCG8VM5ciY5iF2ezGomU/pCX9SW6HLfn9XO7RITmFiwRHl8ty6TEMb3juiHPjyFL6OHamueA/UMe6Pbdfp3qkSUCvAdooJT+0vZydqr1hGS3WBkTGdbRncuTxACA6tCe1eeNAoGABeQZWBOW/EvUx7k3lEYr51xWK+R8Lk3xibvg1oiNKtmj+9A+p9Adt5l8z6Q1vwnV10qPbDIy0ja8wkcX7mqCMDZf7A5yEQtvQFNbbk64iwLSPx2H207vn6lVimiSskAUZ1eVFMTxzIddaqmOxzEng6y6gfBOVK0xksgLNnV+NKw=" ], - "certificate" : [ "MIICpzCCAY8CBgGPFxVYJTANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wHhcNMjQwNDI1MjEwNTI1WhcNMzQwNDI1MjEwNzA1WjAXMRUwEwYDVQQDDAxzcWxwYWdlX2RlbW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4/h+gWt+2YIQmZnvjQP5Oin17MLuCcVUaZfExs8WYZvwcCpFpvalMrLkJG/p70MgbKuQQp9uQszxWPVeBaD/7tgSi8NzTy/tuIfIdfUuWitVNxlDrDt9dh2rreIgAJXYHUeqE6B32QamTCmUlFhIYbX52DX5jJLGCGLOJLcwh8a7AvuxNMCw54+suPZjr8whQOqW7ZtdwZePwIHtJxbL+B2E67x8A8sqeIYg6FCejuZ7m1uPizfhWioHLW30K5Z7ESL40is8l166bkQM8nTH8EIbOUcGgDUirsyGaEp4UUBGZD65s98d1R++7KzCPh8oPbDftxlPw+1SB3d4yyxBdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBALKVXfayldWsdDjwtUZOtzu9fTU3YbUekvkuYr0Fvs9348ZiWoPvt6JQ9i7ytqDxok9CvgVL347ZS+lDkMKhBpw7ryVS0bG/Vg7DeNmjmutGuGdiJR2nUF8z6SgDVWXqr4XrcDy6xwfVtAazc8MXau+eQozlZBiLV4bKDD793m9zqPeSIIipDozMrfKm4jYnam33d9pRQFGDgEGHqXiwR96x8tC5zlFjngKlX1IgigYqARSsOMaV4vU2aIhIq3bLpvSIGGSDo9iw6iYhBYn9tpmtsHCU/RFqsWPhglU168+0VQesCQphKCXoOZp3qIGnRUVySNZSZrHynQ/wLzI1Dos=" ], - "priority" : [ "100" ] - } - }, { - "id" : "2b7d2bdf-aee4-4f31-8bec-97bd01182220", - "name" : "hmac-generated-hs512", - "providerId" : "hmac-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "f908ef09-5b02-4d60-8aa9-906f5be4b4b3" ], - "secret" : [ "_PZq7evS8vCHPBBLIRlgHrcXtE46TgjSaao5Yh1LlyjHUyHhxarMYYbenDFELpc7nw3WDWr2U0lS-y0QY7EHySYvf6zx5er1hNwPV78g4kvJUYRKKf9U8OmWlsr2E8bDGKBr547El5HyU11_KWykzvi_dBkqX6LsceQB8guy8t4" ], - "priority" : [ "100" ], - "algorithm" : [ "HS512" ] - } - } ] - }, - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "68f8d935-47fb-42dc-8e23-94cca509c6b1", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "c5c5650b-a462-4b37-9dd9-f1499e369219", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "16124aaf-0384-4d6d-a108-338b5749aa5d", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "3bbf73c5-00f6-4a2a-a2cb-d74f82653fa3", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "b0e4e594-f55d-46be-86fd-676ec72d979e", - "alias" : "Handle Existing Account", - "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "04618969-a055-4e6f-9e41-27c24b037d30", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "079f9946-bc86-4b50-970f-0fde0eebafb0", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "b4056290-6f49-4343-a803-8029b8cab587", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "1b02c9b7-50d4-4001-8648-4674d2b5c83f", - "alias" : "browser", - "description" : "browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "2c52b4db-2764-4930-ba72-eed08a3568f9", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "d4b0242b-f4a2-4e28-9dc5-5ee8bd64c4a7", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "5c90d095-2a79-40c5-a452-98e0cd9402e6", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "87054128-dec5-47c4-a461-0491ac601ee8", - "alias" : "first broker login", - "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - } ] - }, { - "id" : "197b6d93-54f1-46a1-8bc1-fee099e06978", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "ca5c0b1a-51cf-4a79-bb61-5b923eed9bf7", - "alias" : "registration", - "description" : "registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "856a26b0-cf52-4173-94fb-8726c77de2ab", - "alias" : "registration form", - "description" : "registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-terms-and-conditions", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 70, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "ea7cafde-a77b-4fd6-9510-396748960980", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "35a9e22a-3775-4cad-9a8b-d646cd74c8eb", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "4a307cbf-79bc-4125-8eca-4ea98f7cb27b", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" + "clientPolicies": { + "policies": [] } - }, { - "id" : "0a0aaedd-39f6-4dc0-9153-ac6dbd255d3a", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" - } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : true, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "VERIFY_PROFILE", - "name" : "Verify Profile", - "providerId" : "VERIFY_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 90, - "config" : { } - }, { - "alias" : "delete_credential", - "name" : "Delete Credential", - "providerId" : "delete_credential", - "enabled" : true, - "defaultAction" : false, - "priority" : 100, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "firstBrokerLoginFlow" : "first broker login", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaAuthRequestedUserHint" : "login_hint", - "clientOfflineSessionMaxLifespan" : "0", - "oauth2DevicePollingInterval" : "5", - "clientSessionIdleTimeout" : "0", - "clientOfflineSessionIdleTimeout" : "0", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false", - "cibaExpiresIn" : "120", - "oauth2DeviceCodeLifespan" : "600", - "parRequestUriLifespan" : "60", - "clientSessionMaxLifespan" : "0", - "frontendUrl" : "http://localhost:8181", - "acr.loa.map" : "{}" - }, - "keycloakVersion" : "24.0.3", - "userManagedAccessAllowed" : false, - "clientProfiles" : { - "profiles" : [ ] - }, - "clientPolicies" : { - "policies" : [ ] } -} ] \ No newline at end of file +] diff --git a/examples/single sign on/login.sql b/examples/single sign on/login.sql deleted file mode 100644 index bdd7d0a1..00000000 --- a/examples/single sign on/login.sql +++ /dev/null @@ -1,13 +0,0 @@ -set $oauth_state = sqlpage.random_string(32); - -SELECT 'cookie' as component, 'oauth_state' as name, $oauth_state as value; - -select 'redirect' as component, - sqlpage.environment_variable('OIDC_AUTHORIZATION_ENDPOINT') - || '?response_type=code' - || '&client_id=' || sqlpage.url_encode(sqlpage.environment_variable('OIDC_CLIENT_ID')) - || '&redirect_uri=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/oidc_redirect_handler.sql' - || '&state=' || $oauth_state - || '&scope=openid+profile+email' - || '&nonce=' || sqlpage.random_string(32) - as link; \ No newline at end of file diff --git a/examples/single sign on/logout.sql b/examples/single sign on/logout.sql deleted file mode 100644 index 1abbf15b..00000000 --- a/examples/single sign on/logout.sql +++ /dev/null @@ -1,10 +0,0 @@ --- remove the session cookie -select 'cookie' as component, 'session_id' as name, true as remove; --- remove the session from the database -delete from user_sessions where session_id = sqlpage.cookie('session_id') -returning 'redirect' as component, -- redirect the user to the oidc provider to logout - sqlpage.environment_variable('OIDC_END_SESSION_ENDPOINT') - || '?post_logout_redirect_uri=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/' - || '&client_id=' || sqlpage.environment_variable('OIDC_CLIENT_ID') - || '&id_token_hint=' || oidc_token - as link; \ No newline at end of file diff --git a/examples/single sign on/oidc_redirect_handler.sql b/examples/single sign on/oidc_redirect_handler.sql deleted file mode 100644 index 289df9a6..00000000 --- a/examples/single sign on/oidc_redirect_handler.sql +++ /dev/null @@ -1,47 +0,0 @@ --- If the oauth_state cookie does not match the state parameter in the query string, then the request is invalid (CSRF attack) --- and we should redirect the user to the login page. -select 'redirect' as component, '/login.sql' as link where sqlpage.cookie('oauth_state') != $state; - --- Exchange the authorization code for an access token -set $authorization_code_request = json_object( - 'url', sqlpage.environment_variable('OIDC_TOKEN_ENDPOINT'), - 'method', 'POST', - 'headers', json_object( - 'Content-Type', 'application/x-www-form-urlencoded' - ), - 'body', 'grant_type=authorization_code' - || '&code=' || $code - || '&redirect_uri=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/oidc_redirect_handler.sql' - || '&client_id=' || sqlpage.environment_variable('OIDC_CLIENT_ID') - || '&client_secret=' || sqlpage.environment_variable('OIDC_CLIENT_SECRET') -); -set $access_token = sqlpage.fetch($authorization_code_request); - --- Redirect the user to the login page if the access token could not be obtained -select 'redirect' as component, '/login.sql' as link where $access_token->>'error' is not null; - --- At this point we have $access_token which contains {"access_token":"eyJ...", "scope":"openid profile email" } - --- Fetch the user's profile -set $profile_request = json_object( - 'url', sqlpage.environment_variable('OIDC_USERINFO_ENDPOINT'), - 'method', 'GET', - 'headers', json_object( - 'Authorization', 'Bearer ' || ($access_token->>'access_token') - ) -); -set $user_profile = sqlpage.fetch($profile_request); - --- Redirect the user to the login page if the user's profile could not be obtained -select 'redirect' as component, '/login.sql' as link where $user_profile->>'error' is not null; - --- at this point we have $user_profile which contains {"sub":"0cc01234","email_verified":false,"name":"John Smith","preferred_username":"demo","given_name":"John","family_name":"Smith","email":"demo@example.com"} - --- Now we have a valid access token, we can create a session for the user --- in our database -insert into user_sessions(session_id, user_id, email, oidc_token) - values(sqlpage.random_string(32), $user_profile->>'sub', $user_profile->>'email', $access_token->>'id_token') -- you can get additional information like 'name', 'given_name', 'family_name', 'email_verified', 'preferred_username', 'picture' from the user profile - returning 'cookie' as component, 'session_id' as name, session_id as value; - --- Redirect the user to the home page -select 'redirect' as component, '/' as link; \ No newline at end of file diff --git a/examples/single sign on/protected.sql b/examples/single sign on/protected.sql deleted file mode 100644 index 1683ab77..00000000 --- a/examples/single sign on/protected.sql +++ /dev/null @@ -1,11 +0,0 @@ -select 'redirect' as component, '/login.sql' as link -where not exists(select * from user_sessions where session_id = sqlpage.cookie('session_id')); - - -select 'card' as component, 'My secure protected page' as title, 1 as columns; -select - 'Secret video' as title, - 'https://www.youtube.com/embed/mXdgmSdaXkg' as embed, - 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' as allow, - 'iframe' as embed_mode, - '700' as height; \ No newline at end of file diff --git a/examples/single sign on/protected/index.sql b/examples/single sign on/protected/index.sql new file mode 100644 index 00000000..b82ffe3f --- /dev/null +++ b/examples/single sign on/protected/index.sql @@ -0,0 +1,20 @@ +set user_email = sqlpage.user_info('email'); + +select 'shell' as component, 'My secure app' as title, + json_object( + 'title', 'Log Out', + 'link', sqlpage.oidc_logout_url() + ) as menu_item; + +select 'text' as component, + 'You''re in, '|| sqlpage.user_info('name') || ' !' as title, + 'You are logged in as *`' || $user_email || '`*. + +You have access to this protected page. + +![open door](/assets/welcome.jpeg)' + as contents_md; + +select 'list' as component; +select key as title, value as description +from json_each(sqlpage.user_info_token()); diff --git a/examples/single sign on/protected/public/hello.jpeg b/examples/single sign on/protected/public/hello.jpeg new file mode 100644 index 00000000..89ea6898 Binary files /dev/null and b/examples/single sign on/protected/public/hello.jpeg differ diff --git a/examples/single sign on/sqlpage/sqlpage.yaml b/examples/single sign on/sqlpage/sqlpage.yaml new file mode 100644 index 00000000..7eeace52 --- /dev/null +++ b/examples/single sign on/sqlpage/sqlpage.yaml @@ -0,0 +1,6 @@ +oidc_issuer_url: http://localhost:8181/realms/sqlpage_demo # Given by keycloak as the "OpenID Endpoint Configuration" url. +oidc_client_id: sqlpage # configured in keycloak (http://localhost:8181/admin/master/console/#/sqlpage_demo/clients/a2bec2b8-f850-405e-9f26-59063ffa6f08/settings) +oidc_client_secret: qiawfnYrYzsmoaOZT28rRjPPRamfvrYr # For a safer setup, use environment variables to store this +oidc_protected_paths: ["/protected"] # Makes the website root is publicly accessible, requiring authentication only for the /protected path +oidc_public_paths: ["/protected/public"] # Adds an exception for the /protected/public path, which is publicly accessible too +oidc_additional_trusted_audiences: [] # For increased security, reject any token that has more than just the client ID in the "aud" claim diff --git a/examples/splitwise/group.sql b/examples/splitwise/group.sql index acc911ca..54b85de9 100644 --- a/examples/splitwise/group.sql +++ b/examples/splitwise/group.sql @@ -21,7 +21,7 @@ SELECT 'form' as component, 'Add an expense' as title, 'Add' as validate; SELECT 'Description' AS name; SELECT 'Amount' AS name, 'number' AS type; SELECT 'Spent By' AS name, 'select' as type, - json_group_array(json_object("label", name, "value", id)) as options + json_group_array(json_object('label', name, 'value', id)) as options FROM group_member WHERE group_id = $id; -- Insert the expense posted by the form into the database diff --git a/examples/tiny_twitter/README.md b/examples/tiny_twitter/README.md index 6b15cdb4..7ae615d2 100644 --- a/examples/tiny_twitter/README.md +++ b/examples/tiny_twitter/README.md @@ -5,4 +5,4 @@ It is called tweeter because Elon Musk already has the Twitter trademark, even t It was presented at the [2023 PGConf.EU](https://2023.pgconf.eu/) conference. -You can find the slides at https://sql.ophir.dev/pgconf/pgconf-2023.html. \ No newline at end of file +You can find the slides at https://sql-page.com/pgconf/pgconf-2023.html. \ No newline at end of file diff --git a/examples/todo application (PostgreSQL)/batch.sql b/examples/todo application (PostgreSQL)/batch.sql new file mode 100644 index 00000000..28e6d4b2 --- /dev/null +++ b/examples/todo application (PostgreSQL)/batch.sql @@ -0,0 +1,98 @@ +-- Include 'shell.sql' to generate the page header and footer +select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; + +-- Define a Common Table Expression (CTE) named 'updated' +-- CTEs are temporary named result sets, useful for complex queries +-- Here, it's used to perform the update and capture the results in one step +with updated as ( + -- Update the 'todos' table and return the modified rows + -- This approach allows us to both update the data and use it for reporting + update todos set + -- Modify the title based on user input for labels + -- The CASE statements handle different scenarios for label management + title = case + -- If :remove_label is null, we keep the existing title as is + when :remove_label is null then + title + else + -- Remove any existing labels (text within parentheses) + -- This uses a regular expression to strip out (label) from the end + regexp_replace(title, '\s*\(.*\)', '') + end + -- Concatenate the result with a new label if provided + || + case + -- If no new label is provided, we don't add anything + when :new_label is null or :new_label = '' then + '' + else + -- Add the new label in parentheses at the end + ' (' || :new_label || ')' + end + -- Determine which todos to update based on user selection + where + -- Update specific todos if their IDs are in the :todos parameter + -- :todos is a JSON array of todo string IDs, e.g. ["1", "2", "3"] + -- that optionally includes "all" to update all todos + id in ( + -- Parse the JSON array of todo IDs and convert each to integer + -- This allows for multiple todo selection in the UI + select e::int from jsonb_array_elements_text(:todos::jsonb) e + where e != 'all' + ) + -- If 'all' is the only selected, update every todo (by making the where condition always true) + or :todos = '["all"]' + -- Return all updated rows for counting and potential further use + returning * +) +-- Generate an alert component to inform the user about the update result +-- This provides immediate feedback on the operation's outcome +select 'alert' as component, + 'Batch update' as title, + -- Create a dynamic message with the count of updated todos + format('%s todos updated', (select count(*) from updated)) as description +-- Only display the alert if at least one todo was updated +-- This prevents showing unnecessary alerts for no-op updates +where exists (select * from updated); + +-- Create a form component for the batch update interface +-- This sets up the structure for the user input form +select 'form' as component, + 'Batch update' as title, + 'Update all todos' as contents; + +-- Create a select input for choosing which todos to update +-- This allows users to pick multiple todos or all todos for updating +select + 'select' as type, + 'Update these todos' as label, + 'todos[]' as name, + true as multiple, + true as dropdown, + true as required, + -- Combine a static "all" option with dynamic options for each todo + -- This uses JSON functions to build a complex data structure for the UI + -- The JSON structure is used to set the label, value, and selection state for each option + -- The generated JSON looks like this: + -- [{"label":"Update all todos","value":"all","selected":true},{"label":"Todo 1","value":"1","selected":false}] + jsonb_build_array(jsonb_build_object( -- json_build_object takes a list of key-value pairs and returns a JSON object + 'label', 'Update all todos', -- The label of the option + 'value', 'all', -- The value of the option + 'selected', :todos = '["all"]' or :todos is null -- Pre-select 'all' only if it was previously chosen or if :todos is not set (the page was just loaded) + )) || + -- Generate an option for each todo in the database + jsonb_agg(jsonb_build_object( + 'label', title, + 'value', id, + -- Pre-select this todo if it was in the previous selection + 'selected', (id in (select e::int from jsonb_array_elements_text(:todos::jsonb) e where e != 'all')) + )) as options +from todos; + +-- Create a text input for entering a new label +-- This allows users to specify the label to be added to the selected todos +select 'new_label' as name, 'New label' as label; + +-- Create a checkbox for optionally removing existing labels +-- This gives users the choice to strip old labels before adding a new one +select 'checkbox' as type, 'Remove previous labels' as label, 'remove_label' as name; \ No newline at end of file diff --git a/examples/todo application (PostgreSQL)/docker-compose.yml b/examples/todo application (PostgreSQL)/docker-compose.yml index c29ecdc2..5108ac1e 100644 --- a/examples/todo application (PostgreSQL)/docker-compose.yml +++ b/examples/todo application (PostgreSQL)/docker-compose.yml @@ -1,6 +1,6 @@ services: web: - image: lovasoa/sqlpage:main # main is cutting edge, use lovasoa/sqlpage:latest for the latest stable version + image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version ports: - "8080:8080" volumes: diff --git a/examples/todo application (PostgreSQL)/shell.sql b/examples/todo application (PostgreSQL)/shell.sql index 0701c89c..3a1e9bd4 100644 --- a/examples/todo application (PostgreSQL)/shell.sql +++ b/examples/todo application (PostgreSQL)/shell.sql @@ -1,6 +1,7 @@ select 'shell' as component, format ('Todo list (%s)', count(*)) as title, + 'batch' as menu_item, 'timeline' as menu_item from todos; \ No newline at end of file diff --git a/examples/todo application (PostgreSQL)/sqlpage/migrations/README.md b/examples/todo application (PostgreSQL)/sqlpage/migrations/README.md index 9ce6c0b1..b263393c 100644 --- a/examples/todo application (PostgreSQL)/sqlpage/migrations/README.md +++ b/examples/todo application (PostgreSQL)/sqlpage/migrations/README.md @@ -3,7 +3,7 @@ SQLPage migrations are SQL scripts that you can use to create or update the database schema. They are entirely optional: you can use SQLPage without them, and manage the database schema yourself with other tools. -If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql.ophir.dev/your-first-sql-website/migrations.sql). +If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql-page.com/your-first-sql-website/migrations.sql). ## Creating a migration diff --git a/examples/todo application (PostgreSQL)/sqlpage/sqlpage.json b/examples/todo application (PostgreSQL)/sqlpage/sqlpage.json deleted file mode 100644 index 086aa292..00000000 --- a/examples/todo application (PostgreSQL)/sqlpage/sqlpage.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "database_url": "sqlite://./sqlpage/sqlpage.db?mode=rwc" -} diff --git a/examples/todo application (PostgreSQL)/sqlpage/templates/README.md b/examples/todo application (PostgreSQL)/sqlpage/templates/README.md index b1521b0a..c70a3ace 100644 --- a/examples/todo application (PostgreSQL)/sqlpage/templates/README.md +++ b/examples/todo application (PostgreSQL)/sqlpage/templates/README.md @@ -7,14 +7,14 @@ SQLPage templates are handlebars[^1] files that are used to render the results o ## Default components SQLPage comes with a set of default[^2] components that you can use without having to write any code. -These are documented on https://sql.ophir.dev/components.sql +These are documented on https://sql-page.com/components.sql ## Custom components -You can [write your own component templates](https://sql.ophir.dev/custom_components.sql) +You can [write your own component templates](https://sql-page.com/custom_components.sql) and place them in the `sqlpage/templates` directory. To override a default component, create a file with the same name as the default component. If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory in the SQLPage source code[^2]. -[^2]: A simple component to start from: https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file +[^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file diff --git a/examples/todo application/README.md b/examples/todo application/README.md index 85e265b5..1e1fc200 100644 --- a/examples/todo application/README.md +++ b/examples/todo application/README.md @@ -17,10 +17,10 @@ It will be loaded when the user visits the root of the application In order, it uses: -- the [`dynamic`](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component to load the [`shell.sql`](#shellsql) file that will be used at the top of every page +- the [`dynamic`](https://sql-page.com/documentation.sql?component=dynamic#component) component to load the [`shell.sql`](#shellsql) file that will be used at the top of every page in the application to create a consistent layout and top bar. -- the [`list`](https://sql.ophir.dev/documentation.sql?component=list#component) component to display the list of todo items. -- the [`button`](https://sql.ophir.dev/documentation.sql?component=button#component) component to create a button that will redirect the user to the [`todo_form.sql`](#todo_formsql) page to create a new todo item when clicked. +- the [`list`](https://sql-page.com/documentation.sql?component=list#component) component to display the list of todo items. +- the [`button`](https://sql-page.com/documentation.sql?component=button#component) component to create a button that will redirect the user to the [`todo_form.sql`](#todo_formsql) page to create a new todo item when clicked. ### [`todo_form.sql`](./todo_form.sql) @@ -28,9 +28,9 @@ This file is used to create a new todo item or edit an existing one. It uses: -1. the [`redirect`](https://sql.ophir.dev/documentation.sql?component=redirect#component) component to redirect the user back to the [`index.sql`](#indexsql) page after the form is submitted. -1. the [`dynamic`](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component to load [`shell.sql`](#shellsql) to create a consistent layout and top bar. -1. the [`form`](https://sql.ophir.dev/documentation.sql?component=form#component) component to create a form with fields for the title and description of the todo item. +1. the [`redirect`](https://sql-page.com/documentation.sql?component=redirect#component) component to redirect the user back to the [`index.sql`](#indexsql) page after the form is submitted. +1. the [`dynamic`](https://sql-page.com/documentation.sql?component=dynamic#component) component to load [`shell.sql`](#shellsql) to create a consistent layout and top bar. +1. the [`form`](https://sql-page.com/documentation.sql?component=form#component) component to create a form with fields for the title and description of the todo item. The order of the components is important, as the `redirect` component cannot be used after the page has been displayed. It is called first to ensure that the user is redirected immediately after submitting the form. It is guarded by a `WHERE :todo_id IS NOT NULL` clause to ensure that it only redirects when the form was submitted, not when the page is @@ -80,13 +80,13 @@ The detailed step by step explanation of the delete process is as follows: This file is not meant to be accessed directly by the user (it would display an empty page with only the top bar). But it is included from all the other pages to -call the [`shell`](https://sql.ophir.dev/documentation.sql?component=shell#component) component with the exact same parameters on every page. +call the [`shell`](https://sql-page.com/documentation.sql?component=shell#component) component with the exact same parameters on every page. -It is included everywhere using the [`dynamic`](https://sql.ophir.dev/documentation.sql?component=dynamic#component) component and the [`sqlpage.run_sql`](https://sql.ophir.dev/functions.sql?function=run_sql#function) function. +It is included everywhere using the [`dynamic`](https://sql-page.com/documentation.sql?component=dynamic#component) component and the [`sqlpage.run_sql`](https://sql-page.com/functions.sql?function=run_sql#function) function. ## Running the example -To run the example, simply [download the latest SQLPage release](https://github.com/lovasoa/SQLpage/releases) and run it from the root folder of the example. +To run the example, simply [download the latest SQLPage release](https://github.com/sqlpage/SQLPage/releases) and run it from the root folder of the example. ## SQLPage features used @@ -95,14 +95,14 @@ of the common features of SQLPage. ### Components -- [list](https://sql.ophir.dev/documentation.sql?component=list#component) -- [button](https://sql.ophir.dev/documentation.sql?component=button#component) -- [form](https://sql.ophir.dev/documentation.sql?component=form#component) -- [redirect](https://sql.ophir.dev/documentation.sql?component=redirect#component) -- [shell](https://sql.ophir.dev/documentation.sql?component=shell#component) -- [timeline](https://sql.ophir.dev/documentation.sql?component=timeline#component) -- [dynamic](https://sql.ophir.dev/documentation.sql?component=timeline#component) +- [list](https://sql-page.com/documentation.sql?component=list#component) +- [button](https://sql-page.com/documentation.sql?component=button#component) +- [form](https://sql-page.com/documentation.sql?component=form#component) +- [redirect](https://sql-page.com/documentation.sql?component=redirect#component) +- [shell](https://sql-page.com/documentation.sql?component=shell#component) +- [timeline](https://sql-page.com/documentation.sql?component=timeline#component) +- [dynamic](https://sql-page.com/documentation.sql?component=timeline#component) ### Functions -- [sqlpage.run_sql](https://sql.ophir.dev/functions.sql?function=run_sql#function) +- [sqlpage.run_sql](https://sql-page.com/functions.sql?function=run_sql#function) diff --git a/examples/todo application/sqlpage/migrations/README.md b/examples/todo application/sqlpage/migrations/README.md index 9ce6c0b1..b263393c 100644 --- a/examples/todo application/sqlpage/migrations/README.md +++ b/examples/todo application/sqlpage/migrations/README.md @@ -3,7 +3,7 @@ SQLPage migrations are SQL scripts that you can use to create or update the database schema. They are entirely optional: you can use SQLPage without them, and manage the database schema yourself with other tools. -If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql.ophir.dev/your-first-sql-website/migrations.sql). +If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql-page.com/your-first-sql-website/migrations.sql). ## Creating a migration diff --git a/examples/todo application/sqlpage/templates/README.md b/examples/todo application/sqlpage/templates/README.md index b1521b0a..c70a3ace 100644 --- a/examples/todo application/sqlpage/templates/README.md +++ b/examples/todo application/sqlpage/templates/README.md @@ -7,14 +7,14 @@ SQLPage templates are handlebars[^1] files that are used to render the results o ## Default components SQLPage comes with a set of default[^2] components that you can use without having to write any code. -These are documented on https://sql.ophir.dev/components.sql +These are documented on https://sql-page.com/components.sql ## Custom components -You can [write your own component templates](https://sql.ophir.dev/custom_components.sql) +You can [write your own component templates](https://sql-page.com/custom_components.sql) and place them in the `sqlpage/templates` directory. To override a default component, create a file with the same name as the default component. If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory in the SQLPage source code[^2]. -[^2]: A simple component to start from: https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file +[^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file diff --git a/examples/user-authentication/README.md b/examples/user-authentication/README.md index e61afca5..521ec0a1 100644 --- a/examples/user-authentication/README.md +++ b/examples/user-authentication/README.md @@ -14,7 +14,7 @@ This demonstrates how to implement: - [a logout button](./logout.sql) - [secured pages](./protected_page.sql) that can only be accessed by logged-in users -User authentication is a complex topic, and you can follow the work on implementing differenet authentication methods in [this issue](https://github.com/lovasoa/SQLpage/issues/12). +User authentication is a complex topic, and you can follow the work on implementing differenet authentication methods in [this issue](https://github.com/sqlpage/SQLPage/issues/12). ## How to run @@ -53,7 +53,7 @@ You could restrict user creation to existing administrators and create an initia The [login form](./signin.sql) is a simple form that is handled by [`login.sql`](./login.sql). -`login.sql` checks that the username exists and that the password is correct using the [authentication component](https://sql.ophir.dev/documentation.sql?component=authentication#component) extension with +`login.sql` checks that the username exists and that the password is correct using the [authentication component](https://sql-page.com/documentation.sql?component=authentication#component) extension with ```sql SELECT 'authentication' AS component, @@ -75,10 +75,10 @@ The user is then redirected to [`./protected_page.sql`](./protected_page.sql) wh Protected pages are pages that can only be accessed by logged-in users. There is an example in [`protected_page.sql`](./protected_page.sql) that uses -the [`redirect`](https://sql.ophir.dev/documentation.sql?component=redirect#component) +the [`redirect`](https://sql-page.com/documentation.sql?component=redirect#component) component to redirect the user to the login page if they are not logged in. -Checking whether the user is logged in is as simple as checking that session id returned by [`sqlpage.cookie('session')`](https://sql.ophir.dev/functions.sql?function=cookie#function) exists in the [`login_session`](./sqlpage/migrations/0000_init.sql) table. +Checking whether the user is logged in is as simple as checking that session id returned by [`sqlpage.cookie('session')`](https://sql-page.com/functions.sql?function=cookie#function) exists in the [`login_session`](./sqlpage/migrations/0000_init.sql) table. ### User logout diff --git a/examples/user-authentication/docker-compose.yml b/examples/user-authentication/docker-compose.yml index c29ecdc2..96b78c0e 100644 --- a/examples/user-authentication/docker-compose.yml +++ b/examples/user-authentication/docker-compose.yml @@ -1,6 +1,8 @@ services: web: - image: lovasoa/sqlpage:main # main is cutting edge, use lovasoa/sqlpage:latest for the latest stable version + image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version + build: + context: "../.." ports: - "8080:8080" volumes: diff --git a/examples/user-authentication/signin.sql b/examples/user-authentication/signin.sql index 4057e44d..bab0e883 100644 --- a/examples/user-authentication/signin.sql +++ b/examples/user-authentication/signin.sql @@ -1,14 +1,9 @@ -SELECT 'form' AS component, +SELECT 'login' AS component, + 'login.sql' AS action, 'Sign in' AS title, - 'Sign in' AS validate, - 'login.sql' AS action; - -SELECT 'username' AS name; -SELECT 'password' AS name, 'password' AS type; - -SELECT 'alert' as component, - 'Sorry' as title, - 'We could not authenticate you. Please log in or [create an account](signup.sql).' as description_md, - 'alert-circle' as icon, - 'red' as color -WHERE $error IS NOT NULL; \ No newline at end of file + 'Username' AS username, + 'Password' AS password, + 'user' AS username_icon, + 'lock' AS password_icon, + case when $error is not null then 'We could not authenticate you. Please log in or [create an account](signup.sql).' end as error_message_md, + 'Sign in' AS validate; \ No newline at end of file diff --git a/examples/using react and other custom scripts and styles/my_react_component.js b/examples/using react and other custom scripts and styles/my_react_component.js index b65e4988..5c85f1c4 100644 --- a/examples/using react and other custom scripts and styles/my_react_component.js +++ b/examples/using react and other custom scripts and styles/my_react_component.js @@ -1,26 +1,29 @@ // Here we are using React and ReactDOM directly, but this file could be a compiled // version of a React component written in JSX. -function MyComponent({ greeting_name }) { - const [count, setCount] = React.useState(0); - return React.createElement( - 'button', - { - onClick: async () => { - const r = await fetch('/api.sql'); - const { total_clicks } = await r.json(); - setCount(total_clicks) - }, - className: 'btn btn-primary' - }, - count == 0 - ? `Hello, ${greeting_name}. Click me !` - : `You clicked me ${count} times!` - ); +function _MyComponent({ greeting_name }) { + const [count, setCount] = React.useState(0); + return React.createElement( + "button", + { + type: "button", + onClick: async () => { + const r = await fetch("/api.sql"); + const { total_clicks } = await r.json(); + setCount(total_clicks); + }, + className: "btn btn-primary", + }, + count === 0 + ? `Hello, ${greeting_name}. Click me !` + : `You clicked me ${count} times!`, + ); } -for (const container of document.getElementsByClassName('react_component')) { - const root = ReactDOM.createRoot(container); - const props = JSON.parse(container.dataset.props); - root.render(React.createElement(window[props.react_component_name], props, null)); -} \ No newline at end of file +for (const container of document.getElementsByClassName("react_component")) { + const root = ReactDOM.createRoot(container); + const props = JSON.parse(container.dataset.props); + root.render( + React.createElement(window[props.react_component_name], props, null), + ); +} diff --git a/examples/using react and other custom scripts and styles/style.css b/examples/using react and other custom scripts and styles/style.css index d3fefeff..d40294ed 100644 --- a/examples/using react and other custom scripts and styles/style.css +++ b/examples/using react and other custom scripts and styles/style.css @@ -16,18 +16,25 @@ } @keyframes neon-glow { - 0%, 100% { - text-shadow: 0 0 10px #fa2dd1, 0 0 20px #b30890, 2px 3px 30px #ff00cc; + 0%, + 100% { + text-shadow: + 0 0 10px #fa2dd1, + 0 0 20px #b30890, + 2px 3px 30px #ff00cc; } 50% { - text-shadow: 0 0 1px #e48fd3, 0 0 2px #ca28aa, 0 0 8px #ff00cc; + text-shadow: + 0 0 1px #e48fd3, + 0 0 2px #ca28aa, + 0 0 8px #ff00cc; } } #funky_text:hover { - animation: neon-glow .5s ease-in-out infinite; + animation: neon-glow 0.5s ease-in-out infinite; } #funky_text * { - color: inherit; -} \ No newline at end of file + color: inherit; +} diff --git a/examples/web servers - apache/README.md b/examples/web servers - apache/README.md new file mode 100644 index 00000000..146c0cf7 --- /dev/null +++ b/examples/web servers - apache/README.md @@ -0,0 +1,61 @@ +# SQLPage with Apache Reverse Proxy + +This example demonstrates how to run SQLPage behind the popular Apache HTTP Server. +This is particularly useful when you already have a server running Apache (with a PHP application for example) +and you want to add a SQLPage application. + +This setup allows you to: +- Host multiple websites/applications on a single server +- Serve static files directly through Apache +- Route specific paths to SQLPage + +## How it Works + +Apache acts as a reverse proxy, forwarding requests for `/my_website` to the SQLPage +application while serving static content directly. The configuration uses: + +- [`mod_proxy`](https://httpd.apache.org/docs/current/mod/mod_proxy.html) and [`mod_proxy_http`](https://httpd.apache.org/docs/current/mod/mod_proxy_http.html) for reverse proxy functionality +- [Virtual hosts](https://httpd.apache.org/docs/current/vhosts/) for domain-based routing +- [`ProxyPass`](https://httpd.apache.org/docs/current/mod/mod_proxy.html#proxypass) directives to forward specific paths + +## Docker Setup + +The `docker-compose.yml` defines three services: +- `apache`: Serves static content and routes requests +- `sqlpage`: Handles dynamic content generation +- `mysql`: Provides database storage + +## Native Apache Setup + +To use this with a native Apache installation instead of Docker: + +1. Install Apache and required modules: +```bash +sudo apt install apache2 +sudo a2enmod proxy proxy_http +``` + +2. Configuration changes: +- Place the `httpd.conf` content in `/etc/apache2/sites-available/my-site.conf` +- Adjust paths: + - Change `/var/www` to your static files location + - Update SQLPage URL to match your actual SQLPage server address (`http://localhost:8080/my_website` if you are running sqlpage locally) + - Modify log paths to standard Apache locations (`/var/log/apache2/`) + +3. SQLPage setup: +- Install SQLPage on your server +- Configure it with the same `site_prefix` in `sqlpage.json` +- Ensure MySQL is accessible from the SQLPage instance + +4. Enable the site: +```bash +sudo a2ensite my-site +sudo systemctl reload apache2 +``` + +## Files Overview + +- `httpd.conf`: Apache configuration with proxy rules +- `sqlpage_config/sqlpage.json`: SQLPage configuration with URL prefix +- `static/`: Static files served directly by Apache +- `website/`: SQLPage SQL files for dynamic content diff --git a/examples/web servers - apache/apache/httpd.conf b/examples/web servers - apache/apache/httpd.conf new file mode 100644 index 00000000..fa659b6b --- /dev/null +++ b/examples/web servers - apache/apache/httpd.conf @@ -0,0 +1,39 @@ +LoadModule mpm_prefork_module modules/mod_mpm_prefork.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule dir_module modules/mod_dir.so + + + User daemon + Group daemon + + +ServerName localhost +Listen 80 + +DirectoryIndex index.html + +ErrorLog /proc/self/fd/2 +LogLevel warn +CustomLog /proc/self/fd/1 combined + + + ServerName my_website + DocumentRoot "/var/www" + + ProxyPreserveHost On + + + Require all granted + Options Indexes FollowSymLinks + AllowOverride None + + + + ProxyPass "http://sqlpage:8080/my_website" + ProxyPassReverse "http://sqlpage:8080/my_website" + + \ No newline at end of file diff --git a/examples/web servers - apache/docker-compose.yml b/examples/web servers - apache/docker-compose.yml new file mode 100644 index 00000000..f99942d5 --- /dev/null +++ b/examples/web servers - apache/docker-compose.yml @@ -0,0 +1,33 @@ +services: + sqlpage: + image: lovasoa/sqlpage:main + volumes: + - ./sqlpage_config:/etc/sqlpage:ro + - ./website:/var/www:ro + environment: + - DATABASE_URL=mysql://sqlpage:sqlpage_password@mysql:3306/sqlpage_db + depends_on: + - mysql + + apache: + image: httpd:2.4 + ports: + - "80:80" + volumes: + - ./apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro + - ./static:/var/www:ro + depends_on: + - sqlpage + + mysql: + image: mysql:8 + environment: + - MYSQL_ROOT_PASSWORD=root_password + - MYSQL_DATABASE=sqlpage_db + - MYSQL_USER=sqlpage + - MYSQL_PASSWORD=sqlpage_password + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: \ No newline at end of file diff --git a/examples/web servers - apache/sqlpage_config/sqlpage.json b/examples/web servers - apache/sqlpage_config/sqlpage.json new file mode 100644 index 00000000..76f13c26 --- /dev/null +++ b/examples/web servers - apache/sqlpage_config/sqlpage.json @@ -0,0 +1,3 @@ +{ + "site_prefix": "/my_website" +} diff --git a/examples/web servers - apache/static/index.html b/examples/web servers - apache/static/index.html new file mode 100644 index 00000000..31970770 --- /dev/null +++ b/examples/web servers - apache/static/index.html @@ -0,0 +1,42 @@ + + + + + + Welcome + + + +

Welcome to Our Site

+

+ This page is served by Apache web server, which acts as a reverse proxy. When you click the button below, + you'll be redirected to a SQLPage application running in a separate container. The setup includes three + services: Apache for static content and routing, SQLPage for dynamic content, and MySQL for data storage. +

+ Enter Site + + diff --git a/examples/web servers - apache/website/index.sql b/examples/web servers - apache/website/index.sql new file mode 100644 index 00000000..d3d9438c --- /dev/null +++ b/examples/web servers - apache/website/index.sql @@ -0,0 +1,9 @@ +select + 'text' as component, + true as article, + ' +# Welcome to my website + +Using SQLPage v' || sqlpage.version() || ' + +Connected to **MySQL** v' || version () as contents_md; diff --git a/lambda.Dockerfile b/lambda.Dockerfile index 27cef7f7..c91ef7f5 100644 --- a/lambda.Dockerfile +++ b/lambda.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.80-alpine as builder +FROM rust:1.91-alpine AS builder RUN rustup component add clippy rustfmt RUN apk add --no-cache musl-dev zip WORKDIR /usr/src/sqlpage @@ -13,7 +13,7 @@ RUN mv target/release/sqlpage bootstrap && \ ldd bootstrap && \ zip -9 -r deploy.zip bootstrap index.sql -FROM public.ecr.aws/lambda/provided:al2 as runner +FROM public.ecr.aws/lambda/provided:al2 AS runner COPY --from=builder /usr/src/sqlpage/bootstrap /main COPY --from=builder /usr/src/sqlpage/index.sql ./index.sql ENTRYPOINT ["/main"] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..44edd9c8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,179 @@ +{ + "name": "sqlpage", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlpage", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^2.1.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.3.tgz", + "integrity": "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.1.3", + "@biomejs/cli-darwin-x64": "2.1.3", + "@biomejs/cli-linux-arm64": "2.1.3", + "@biomejs/cli-linux-arm64-musl": "2.1.3", + "@biomejs/cli-linux-x64": "2.1.3", + "@biomejs/cli-linux-x64-musl": "2.1.3", + "@biomejs/cli-win32-arm64": "2.1.3", + "@biomejs/cli-win32-x64": "2.1.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.3.tgz", + "integrity": "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.3.tgz", + "integrity": "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.3.tgz", + "integrity": "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.3.tgz", + "integrity": "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.3.tgz", + "integrity": "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.3.tgz", + "integrity": "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.3.tgz", + "integrity": "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.3.tgz", + "integrity": "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..2e53fb73 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "sqlpage", + "version": "1.0.0", + "scripts": { + "test": "biome check .", + "format": "biome format --write .", + "fix": "biome check --fix --unsafe ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sqlpage/SQLPage.git" + }, + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^2.1.2" + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..59986f41 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,20 @@ +# Docker Build Scripts + +This directory contains scripts used by the Dockerfile to build SQLPage with cross-compilation support. + +## Scripts + +- **`setup-cross-compilation.sh`**: Sets up the cross-compilation environment based on target and build architectures. Handles system dependencies, cross-compiler installation, and libgcc extraction for runtime. +- **`build-dependencies.sh`**: Builds only the project dependencies for Docker layer caching +- **`build-project.sh`**: Builds the final SQLPage binary + +## Usage + +These scripts are automatically copied and executed by the Dockerfile during the build process. They handle: + +- Cross-compilation setup for different architectures (amd64, arm64, arm) +- System dependencies installation +- Cargo build configuration with appropriate linkers +- Library extraction for runtime + +The scripts use temporary files in `/tmp/` to pass configuration between stages and export environment variables for use in subsequent build steps. diff --git a/scripts/build-dependencies.sh b/scripts/build-dependencies.sh new file mode 100755 index 00000000..5a55754a --- /dev/null +++ b/scripts/build-dependencies.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +source /tmp/build-env.sh + +echo "Building dependencies for target: $TARGET" + +cargo build \ + --target "$TARGET" \ + --config "target.$TARGET.linker=\"$LINKER\"" \ + --features odbc-static \ + --profile superoptimized diff --git a/scripts/build-project.sh b/scripts/build-project.sh new file mode 100755 index 00000000..ac595b61 --- /dev/null +++ b/scripts/build-project.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +source /tmp/build-env.sh + +echo "Building project for target: $TARGET" + +cargo build \ + --target "$TARGET" \ + --config "target.$TARGET.linker=\"$LINKER\"" \ + --features odbc-static \ + --profile superoptimized + +mv "target/$TARGET/superoptimized/sqlpage" sqlpage.bin diff --git a/scripts/setup-cross-compilation.sh b/scripts/setup-cross-compilation.sh new file mode 100755 index 00000000..83531abb --- /dev/null +++ b/scripts/setup-cross-compilation.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -euo pipefail + +TARGETARCH="$1" +BUILDARCH="$2" +BINDGEN_EXTRA_CLANG_ARGS="" + +apt-get update + +if [ "$TARGETARCH" = "$BUILDARCH" ]; then + TARGET="$(rustup target list --installed | head -n1)" + LINKER="gcc" + apt-get install -y gcc libgcc-s1 make + LIBDIR="/lib/$(gcc -print-multiarch)" +elif [ "$TARGETARCH" = "arm64" ]; then + TARGET="aarch64-unknown-linux-gnu" + LINKER="aarch64-linux-gnu-gcc" + apt-get install -y gcc-aarch64-linux-gnu libgcc-s1-arm64-cross make + LIBDIR="/usr/aarch64-linux-gnu/lib" +elif [ "$TARGETARCH" = "arm" ]; then + TARGET="armv7-unknown-linux-gnueabihf" + LINKER="arm-linux-gnueabihf-gcc" + apt-get install -y gcc-arm-linux-gnueabihf libgcc-s1-armhf-cross make cmake libclang-dev + cargo install --force --locked bindgen-cli + SYSROOT=$(arm-linux-gnueabihf-gcc -print-sysroot) + BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$SYSROOT -I$SYSROOT/usr/include -I$SYSROOT/usr/include/arm-linux-gnueabihf" + LIBDIR="/usr/arm-linux-gnueabihf/lib" +else + echo "Unsupported cross compilation target: $TARGETARCH" + exit 1 +fi + +mkdir -p /tmp/sqlpage-libs +cp "$LIBDIR/libgcc_s.so.1" /tmp/sqlpage-libs/ +rustup target add "$TARGET" + +{ + echo "export TARGET='$TARGET'" + echo "export LINKER='$LINKER'" + if [ -n "$BINDGEN_EXTRA_CLANG_ARGS" ]; then + printf "export BINDGEN_EXTRA_CLANG_ARGS=%q\n" "$BINDGEN_EXTRA_CLANG_ARGS" + fi +} > /tmp/build-env.sh diff --git a/sqlpage.service b/sqlpage.service index e51af237..ea65debb 100644 --- a/sqlpage.service +++ b/sqlpage.service @@ -4,7 +4,7 @@ [Unit] Description=SQLPage website -Documentation=https://sql.ophir.dev +Documentation=https://sql-page.com After=network.target [Service] diff --git a/sqlpage/apexcharts.js b/sqlpage/apexcharts.js index 380685fc..a0f46367 100644 --- a/sqlpage/apexcharts.js +++ b/sqlpage/apexcharts.js @@ -1,199 +1,301 @@ -/* !include https://cdn.jsdelivr.net/npm/apexcharts@3.52.0/dist/apexcharts.min.js */ - - -function sqlpage_chart() { - - const tblrColors = Object.fromEntries(['azure', 'red', 'lime', 'purple', 'yellow', 'blue', 'gray-600', 'orange', 'black', 'pink', 'teal', 'indigo', 'cyan', 'green', 'cyan'] - .map(c => [c, getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + c)])); - - /** @typedef { { [name:string]: {data:{x:number,y:number}[], name:string} } } Series */ - - /** - * @param {Series} series - * @returns {Series} */ - function align_categories(series) { - const new_series = series.map(s => ({ name: s.name, data: [] })); - do { - var category = null; - series.forEach((s, s_i) => { - const point = s.data[0]; - let new_point = { x: category, y: 0 }; - if (point) { - if (category == null) category = point.x; - if (category === point.x) { - new_point = s.data.shift(); - } - } - new_series[s_i].data.push(new_point); - }) - new_series.forEach(s => s.data[s.data.length - 1].x = category); - } while (category != null); - new_series.forEach(s => s.data.pop()); - return new_series; - } +/* !include https://cdn.jsdelivr.net/npm/apexcharts@5.3.6/dist/apexcharts.min.js */ +sqlpage_chart = (() => { + function sqlpage_chart() { for (const c of document.querySelectorAll("[data-pre-init=chart]")) { - try { - const [data_element] = c.getElementsByTagName("data"); - const data = JSON.parse(data_element.textContent); - const chartContainer = c.querySelector('.chart'); - chartContainer.innerHTML = ""; - const is_timeseries = !!data.time; - /** @type { Series } */ - const series_map = {}; - data.points.forEach(([name, x, y, z]) => { - series_map[name] = series_map[name] || { name, data: [] } - if (is_timeseries) x = - (typeof x === 'number') - ? new Date(x * 1000) // databases use seconds; JS uses ms - : new Date(x); - series_map[name].data.push({ x, y, z }); - }) - if (data.xmin == null) data.xmin = undefined; - if (data.xmax == null) data.xmax = undefined; - if (data.ymin == null) data.ymin = undefined; - if (data.ymax == null) data.ymax = undefined; - - const colors = [ - ...data.colors.filter(c => c).map(c => tblrColors[c]), - ...Object.values(tblrColors) - ]; - - let series = Object.values(series_map); - - // tickamount is the number of intervals, not the number of ticks - const tickAmount = data.xticks || - Math.min(30, Math.max(...series.map(s => s.data.length - 1))); - - let labels; - const categories = series.length > 0 && typeof series[0].data[0].x === "string"; - if (data.type === "pie") { - labels = data.points.map(([name, x, y]) => x || name); - series = data.points.map(([name, x, y]) => y); - } else if (categories && data.type !== 'line' && data.type !== 'treemap') series = align_categories(series); - - const options = { - chart: { - type: data.type || 'line', - fontFamily: 'inherit', - parentHeightOffset: 0, - height: chartContainer.style.height, - stacked: !!data.stacked, - toolbar: { - show: !!data.toolbar, - }, - animations: { - enabled: false - }, - zoom: { - enabled: false - } - }, - theme: { - palette: 'palette4', - }, - dataLabels: { - enabled: !!data.labels, - }, - fill: { - type: data.type === 'area' ? 'gradient' : 'solid', - }, - stroke: { - width: data.type === 'area' ? 3 : 1, - lineCap: "round", - curve: "smooth", - }, - xaxis: { - tooltip: { - enabled: false - }, - min: data.xmin, - max: data.xmax, - tickAmount, - title: { - text: data.xtitle || undefined, - }, - type: is_timeseries ? 'datetime' : categories ? 'category' : undefined, - labels: { - datetimeUTC: false, - } - }, - yaxis: { - logarithmic: !!data.logarithmic, - min: data.ymin, - max: data.ymax, - stepSize: data.ystep, - tickAmount: data.yticks, - title: { - text: data.ytitle || undefined, - } - }, - zaxis: { - title: { - text: data.ztitle || undefined, - } - }, - markers: { - size: data.marker || 0, - strokeWidth: 0, - hover: { - sizeOffset: 5, - } - }, - tooltip: { - fillSeriesColor: false, - custom: (data.type === 'bubble' || data.type === 'scatter') ? bubbleTooltip : undefined, - }, - plotOptions: { - bar: { horizontal: !!data.horizontal }, - bubble: { minBubbleRadius: 5, }, - }, - colors, - series, - }; - if (labels) options.labels = labels; - const chart = new ApexCharts(chartContainer, options); - chart.render(); - if (window.charts) window.charts.push(chart); - else window.charts = [chart]; - c.removeAttribute("data-pre-init"); - } catch (e) { console.log(e) } + try { + build_sqlpage_chart(c); + } catch (e) { + console.error(e); + } } -} + } + + const tblrColors = [ + ["blue", "#1c7ed6", "#339af0"], + ["red", "#f03e3e", "#ff6b6b"], + ["green", "#37b24d", "#51cf66"], + ["pink", "#d6336c", "#f06595"], + ["purple", "#ae3ec9", "#cc5de8"], + ["orange", "#f76707", "#ff922b"], + ["cyan", "#1098ad", "#22b8cf"], + ["teal", "#0ca678", "#20c997"], + ["yellow", "#f59f00", "#fcc419"], + ["indigo", "#4263eb", "#5c7cfa"], + ["lime", "#74b816", "#94d82d"], + ["azure", "#339af0", "#339af0"], + ["gray", "#495057", "#adb5bd"], + ["black", "#000000", "#000000"], + ["white", "#ffffff", "#f8f9fa"], + ]; + const colorNames = Object.fromEntries( + tblrColors.flatMap(([name, dark, light]) => [ + [name, dark], + [`${name}-lt`, light], + ]), + ); + const isDarkTheme = document.body?.dataset?.bsTheme === "dark"; + + /** @typedef { { [name:string]: {data:{x:number|string|Date,y:number}[], name:string} } } Series */ + + /** + * Aligns series data points by their x-axis categories, ensuring all series have data points + * for each unique category. Missing values are filled with zeros. + * Categories are ordered by their name. + * + * @example + * // Input series: + * const series = [ + * { name: "A", data: [{x: "X2", y: 10}, {x: "X3", y: 30}] }, + * { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}] } + * ]; + * + * // Output after align_categories (orderedCategories will be ["X1","X2", "X3"]): + * // [ + * // { name: "A", data: [{x: "X1", y: 0}, {x: "X2", y: 10}, {x: "X3", y: 30}] }, + * // { name: "B", data: [{x: "X1", y: 25}, {x: "X2", y: 20}, {x: "X3", y: 0}] } + * // ] + * + * @param {(Series[string])[]} series - Array of series objects, each containing name and data points + * @returns {Series[string][]} Aligned series with consistent categories across all series + */ + function align_categories(series) { + const categoriesSet = new Set(); + const pointers = series.map((_) => 0); // Index of current data point in each series + const x_at = (series_idx) => + series[series_idx].data[pointers[series_idx]].x; + const series_idxs = series.flatMap((s, i) => (s.data.length ? i : [])); + while (series_idxs.length > 0) { + let idx_of_xmin = series_idxs[0]; + for (const series_idx of series_idxs) { + if (x_at(series_idx) < x_at(idx_of_xmin)) idx_of_xmin = series_idx; + } -function bubbleTooltip({ series, seriesIndex, dataPointIndex, w }) { + const new_category = x_at(idx_of_xmin); + if (!categoriesSet.has(new_category)) categoriesSet.add(new_category); + pointers[idx_of_xmin]++; + if (pointers[idx_of_xmin] >= series[idx_of_xmin].data.length) { + series_idxs.splice(series_idxs.indexOf(idx_of_xmin), 1); + } + } + // Create a map of category -> value for each series and rebuild + return series.map((s) => { + const valueMap = new Map(s.data.map((point) => [point.x, point.y])); + return { + name: s.name, + data: Array.from(categoriesSet, (category) => ({ + x: category, + y: valueMap.get(category) || 0, + })), + }; + }); + } + + /** @param {HTMLElement} c */ + function build_sqlpage_chart(c) { + const [data_element] = c.getElementsByTagName("data"); + const data = JSON.parse(data_element.textContent); + const chartContainer = c.querySelector(".chart"); + chartContainer.innerHTML = ""; + const is_timeseries = !!data.time; + /** @type { Series } */ + const series_map = {}; + for (const [name, old_x, old_y, z] of data.points) { + series_map[name] = series_map[name] || { name, data: [] }; + let x = old_x; + let y = old_y; + if (is_timeseries) { + if (typeof x === "number") x = new Date(x * 1000); + else if (data.type === "rangeBar" && Array.isArray(y)) + y = y.map((y) => new Date(y).getTime()); + else x = new Date(x); + } + series_map[name].data.push({ x, y, z }); + } + if (data.xmin == null) data.xmin = undefined; + if (data.xmax == null) data.xmax = undefined; + if (data.ymin == null) data.ymin = undefined; + if (data.ymax == null) data.ymax = undefined; + + const colors = [ + ...data.colors.filter((c) => c).map((c) => colorNames[c]), + ...tblrColors.map(([_, dark, light]) => (isDarkTheme ? dark : light)), + ...tblrColors.map(([_, dark, light]) => (isDarkTheme ? light : dark)), + ]; + + let series = Object.values(series_map); + + let labels; + const categories = + series.length > 0 && typeof series[0].data[0].x === "string"; + if (data.type === "pie") { + labels = data.points.map(([name, x, _y]) => x || name); + series = data.points.map(([_name, _x, y]) => Number.parseFloat(y)); + } else if (categories && data.type === "bar" && series.length > 1) + series = align_categories(series); + + const chart_type = data.type || "line"; + const options = { + chart: { + type: chart_type, + fontFamily: "inherit", + parentHeightOffset: 0, + height: chartContainer.style.height, + stacked: !!data.stacked, + toolbar: { + show: !!data.toolbar, + }, + animations: { + enabled: false, + }, + zoom: { + enabled: false, + }, + }, + theme: { + palette: "palette4", + }, + dataLabels: { + enabled: !!data.labels, + dropShadow: { + enabled: true, + color: "var(--tblr-primary-bg-subtle)", + }, + formatter: + data.type === "rangeBar" + ? (_val, { seriesIndex, w }) => w.config.series[seriesIndex].name + : data.type === "pie" + ? (value, { seriesIndex, w }) => + `${w.config.labels[seriesIndex]}: ${value.toFixed()}%` + : (value) => value?.toLocaleString?.() || value, + }, + fill: { + type: data.type === "area" ? "gradient" : "solid", + }, + stroke: { + width: + { + area: 3, + line: 2, + }[chart_type] || 0, + lineCap: "round", + curve: "smooth", + }, + xaxis: { + tooltip: { + enabled: false, + }, + min: data.xmin, + max: data.xmax, + title: { + text: data.xtitle || undefined, + }, + type: is_timeseries ? "datetime" : categories ? "category" : undefined, + labels: { + datetimeUTC: false, + }, + }, + yaxis: { + logarithmic: !!data.logarithmic, + min: data.ymin, + max: data.ymax, + stepSize: data.ystep, + tickAmount: data.yticks, + title: { + text: data.ytitle || undefined, + }, + }, + zaxis: { + title: { + text: data.ztitle || undefined, + }, + }, + markers: { + size: data.marker || 0, + strokeWidth: 0, + hover: { + sizeOffset: 5, + }, + }, + tooltip: { + fillSeriesColor: false, + custom: + data.type === "bubble" || data.type === "scatter" + ? bubbleTooltip + : undefined, + y: { + formatter: (value) => { + if (value == null) return ""; + if (is_timeseries && data.type === "rangeBar") { + const d = new Date(value); + if (d.getHours() === 0 && d.getMinutes() === 0) + return d.toLocaleDateString(); + return d.toLocaleString(); + } + const str_val = value.toLocaleString(); + if (str_val.length > 10 && Number.isNaN(value)) + return value.toFixed(2); + return str_val; + }, + }, + }, + plotOptions: { + bar: { + horizontal: !!data.horizontal || data.type === "rangeBar", + borderRadius: 5, + }, + bubble: { minBubbleRadius: 5 }, + }, + colors, + series, + }; + if (labels) options.labels = labels; + // tickamount is the number of intervals, not the number of ticks + if (data.xticks) options.xaxis.tickAmount = data.xticks; + console.log("Rendering chart", options); + const chart = new ApexCharts(chartContainer, options); + chart.render(); + if (window.charts) window.charts.push(chart); + else window.charts = [chart]; + c.removeAttribute("data-pre-init"); + } + + function bubbleTooltip({ seriesIndex, dataPointIndex, w }) { const { name, data } = w.config.series[seriesIndex]; const point = data[dataPointIndex]; - const tooltip = document.createElement('div'); - tooltip.className = 'apexcharts-tooltip-text'; - tooltip.style.fontFamily = 'inherit'; + const tooltip = document.createElement("div"); + tooltip.className = "apexcharts-tooltip-text"; + tooltip.style.fontFamily = "inherit"; - const seriesName = document.createElement('div'); - seriesName.className = 'apexcharts-tooltip-y-group'; - seriesName.style.fontWeight = 'bold'; + const seriesName = document.createElement("div"); + seriesName.className = "apexcharts-tooltip-y-group"; + seriesName.style.fontWeight = "bold"; seriesName.innerText = name; tooltip.appendChild(seriesName); - for (const axis of ['x', 'y', 'z']) { - const value = point[axis]; - if (value == null) continue; - const axisValue = document.createElement('div'); - axisValue.className = 'apexcharts-tooltip-y-group'; - let axis_conf = w.config[axis + 'axis']; - if (axis_conf.length) axis_conf = axis_conf[0]; - const title = axis_conf.title.text || axis; - const labelSpan = document.createElement('span'); - labelSpan.className = 'apexcharts-tooltip-text-y-label'; - labelSpan.innerText = title + ': '; - axisValue.appendChild(labelSpan); - const valueSpan = document.createElement('span'); - valueSpan.className = 'apexcharts-tooltip-text-y-value'; - valueSpan.innerText = value; - axisValue.appendChild(valueSpan); - tooltip.appendChild(axisValue); + for (const axis of ["x", "y", "z"]) { + const value = point[axis]; + if (value == null) continue; + const axisValue = document.createElement("div"); + axisValue.className = "apexcharts-tooltip-y-group"; + let axis_conf = w.config[`${axis}axis`]; + if (axis_conf.length) axis_conf = axis_conf[0]; + const title = axis_conf.title.text || axis; + const labelSpan = document.createElement("span"); + labelSpan.className = "apexcharts-tooltip-text-y-label"; + labelSpan.innerText = `${title}: `; + axisValue.appendChild(labelSpan); + const valueSpan = document.createElement("span"); + valueSpan.className = "apexcharts-tooltip-text-y-value"; + valueSpan.innerText = value; + axisValue.appendChild(valueSpan); + tooltip.appendChild(axisValue); } return tooltip.outerHTML; -} + } + + return sqlpage_chart; +})(); -add_init_fn(sqlpage_chart); \ No newline at end of file +add_init_fn(sqlpage_chart); diff --git a/sqlpage/migrations/README.md b/sqlpage/migrations/README.md index 9ce6c0b1..9d2765fc 100644 --- a/sqlpage/migrations/README.md +++ b/sqlpage/migrations/README.md @@ -3,7 +3,7 @@ SQLPage migrations are SQL scripts that you can use to create or update the database schema. They are entirely optional: you can use SQLPage without them, and manage the database schema yourself with other tools. -If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql.ophir.dev/your-first-sql-website/migrations.sql). +If you are new to SQL migrations, please read our [**introduction to database migrations**](https://sql-page.com/your-first-sql-website/migrations.sql). ## Creating a migration @@ -21,7 +21,13 @@ that is greater than the previous one. Use commands like `ALTER TABLE` to update the schema declaratively instead of modifying the existing `CREATE TABLE` statements. -If you try to edit an existing migration, SQLPage will not run it again, will detect +If you try to edit an existing migration, SQLPage will not run it again, it will detect that the migration has already executed. Also, if the migration is different than the one that was executed, SQLPage will throw an error as the database structure must match. + +## Creating migrations on the command line + +You can create a migration directly with sqlpage by running the command `sqlpage create-migration [migration_name]` + +For example if you run `sqlpage create-migration "Example Migration 1"` on the command line, you will find a new file under the `sqlpage/migrations` folder called `[timestamp]_example_migration_1.sql` where timestamp is the current time when you ran the command. ## Running migrations diff --git a/sqlpage/sqlpage.css b/sqlpage/sqlpage.css index dd8c5226..aa34cb87 100644 --- a/sqlpage/sqlpage.css +++ b/sqlpage/sqlpage.css @@ -1,11 +1,10 @@ -/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css */ -/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/css/tabler-vendors.min.css */ +/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css */ +/* !include https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.css */ +/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler-vendors.min.css */ -:root { - /* Workaround for https://github.com/tabler/tabler/issues/1879 */ - --tblr-text-secondary-rgb: var(--tblr-secondary-rgb); - --tblr-code-color: #073345; - --tblr-code-bg: #e4f1ff; +.navbar { + /* https://github.com/sqlpage/SQLPage/issues/822 */ + --tblr-navbar-color: rgba(var(--tblr-body-color-rgb), 0.8); } [data-bs-theme="dark"] .alert:not(.alert-important) { @@ -23,17 +22,6 @@ td > p { margin-bottom: 0 !important; } -/* https://github.com/tabler/tabler/issues/1648 */ -* { - scrollbar-color: var(--tblr-primary) var(--tblr-bg-surface-dark) !important; -} -::-webkit-scrollbar { - width: 4px !important; -} -::-webkit-scrollbar-thumb { - background: var(--tblr-primary) !important; -} - .text-secondary a { color: inherit; text-decoration: underline; @@ -44,6 +32,13 @@ td > p { border: 1px solid transparent !important; } +/* remove the ugly text highlight in the default tom-select */ +.ts-dropdown [data-selectable] .highlight { + background: inherit; + color: inherit; + text-decoration: underline; +} + .page { /* Leave space for the footer */ min-height: calc(100% - 3rem); @@ -56,4 +51,143 @@ td > p { code { font-size: 100%; -} \ No newline at end of file +} + +.apexcharts-text, +.apexcharts-datalabel { + fill: var(--tblr-body-color) !important; + font-weight: var(--tblr-body-font-weight); +} + +/** table **/ +.table-freeze-headers thead { + position: sticky; + top: 0; + z-index: 2; +} + +.table-freeze-footers tfoot { + position: sticky; + bottom: 0; + z-index: 2; +} + +.table-freeze-headers { + max-height: 50vh; +} + +.table-freeze-footers { + max-height: 50vh; +} + +.table-freeze-columns th:first-child { + position: sticky; + left: 0; + z-index: 2; +} + +.table-freeze-columns td:first-child { + position: sticky; + left: 0; + background: var(--tblr-bg-surface-secondary); + box-shadow: 3px 0 3px var(--tblr-border-color); +} + +/* Prevent the fixed headers from hiding the selected target row */ +.table-freeze-headers tr[id] { + scroll-margin-top: 2.1rem; +} + +.article-text { + font-size: 1.2em; + line-height: 1.5em; + font-family: "Times New Roman", serif; + width: 65ch; + max-width: 100%; + margin: 1.5em auto; +} + +.article-text p::first-letter { + font-size: 1.2em; + font-weight: 500; +} + +li p { + margin: 0; +} + +.leaflet-container { + background: var(--tblr-active-bg) !important; +} + +/* +See https://github.com/tabler/tabler/issues/2404 +*/ +.status-x { + --tblr-status-color: #000000; + --tblr-status-color-rgb: 0, 0, 0; +} +.status-facebook { + --tblr-status-color: #1877f2; + --tblr-status-color-rgb: 24, 119, 242; +} +.status-twitter { + --tblr-status-color: #1da1f2; + --tblr-status-color-rgb: 29, 161, 242; +} +.status-linkedin { + --tblr-status-color: #0a66c2; + --tblr-status-color-rgb: 10, 102, 194; +} +.status-google { + --tblr-status-color: #dc4e41; + --tblr-status-color-rgb: 220, 78, 65; +} +.status-youtube { + --tblr-status-color: #ff0000; + --tblr-status-color-rgb: 255, 0, 0; +} +.status-vimeo { + --tblr-status-color: #1ab7ea; + --tblr-status-color-rgb: 26, 183, 234; +} +.status-dribbble { + --tblr-status-color: #ea4c89; + --tblr-status-color-rgb: 234, 76, 137; +} +.status-github { + --tblr-status-color: #181717; + --tblr-status-color-rgb: 24, 23, 23; +} +.status-instagram { + --tblr-status-color: #e4405f; + --tblr-status-color-rgb: 228, 64, 95; +} +.status-pinterest { + --tblr-status-color: #bd081c; + --tblr-status-color-rgb: 189, 8, 28; +} +.status-vk { + --tblr-status-color: #6383a8; + --tblr-status-color-rgb: 99, 131, 168; +} +.status-rss { + --tblr-status-color: #ffa500; + --tblr-status-color-rgb: 255, 165, 0; +} +.status-flickr { + --tblr-status-color: #0063dc; + --tblr-status-color-rgb: 0, 99, 220; +} +.status-bitbucket { + --tblr-status-color: #0052cc; + --tblr-status-color-rgb: 0, 82, 204; +} +.status-tabler { + --tblr-status-color: #066fd1; + --tblr-status-color-rgb: 6, 111, 209; +} + +.text-black-fg { + color: var(--tblr-dark-fg) !important; +} diff --git a/sqlpage/sqlpage.db b/sqlpage/sqlpage.db index 3ca97edb..a8946d79 100644 Binary files a/sqlpage/sqlpage.db and b/sqlpage/sqlpage.db differ diff --git a/sqlpage/sqlpage.js b/sqlpage/sqlpage.js index 1115b302..07ccd349 100644 --- a/sqlpage/sqlpage.js +++ b/sqlpage/sqlpage.js @@ -1,59 +1,155 @@ -/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js */ -/* !include https://cdn.jsdelivr.net/npm/list.js-fixed@2.3.4/dist/list.min.js */ - +/* !include https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/js/tabler.min.js */ const nonce = document.currentScript.nonce; function sqlpage_card() { - for (const c of document.querySelectorAll("[data-pre-init=card]")) { - const source = c.dataset.embed; - fetch(c.dataset.embed) - .then(res => res.text()) - .then(html => { - const body = c.querySelector(".card-content"); - body.innerHTML = html; - c.removeAttribute("data-pre-init"); - const spinner = c.querySelector(".card-loading-placeholder"); - if (spinner) { - spinner.parentNode.removeChild(spinner); - } - const fragLoadedEvt = new CustomEvent("fragment-loaded", { - bubbles: true - }); - c.dispatchEvent(fragLoadedEvt); - }) + for (const c of document.querySelectorAll("[data-pre-init=card]")) { + c.removeAttribute("data-pre-init"); + const url = new URL(c.dataset.embed, window.location.href); + url.searchParams.set("_sqlpage_embed", "1"); + fetch(url) + .then((res) => res.text()) + .then((html) => { + const body = c.querySelector(".card-content"); + body.innerHTML = html; + const spinner = c.querySelector(".card-loading-placeholder"); + if (spinner) { + spinner.parentNode.removeChild(spinner); + } + const fragLoadedEvt = new CustomEvent("fragment-loaded", { + bubbles: true, + }); + c.dispatchEvent(fragLoadedEvt); + }); + } +} + +/** @param {HTMLElement} root_el */ +function setup_table(root_el) { + /** @type {HTMLInputElement | null} */ + const search_input = root_el.querySelector("input.search"); + const table_el = root_el.querySelector("table"); + const sort_buttons = [...table_el.querySelectorAll("button.sort[data-sort]")]; + const item_parent = table_el.querySelector("tbody"); + const has_sort = sort_buttons.length > 0; + + if (search_input || has_sort) { + const items = table_parse_data(table_el, sort_buttons); + if (search_input) setup_table_search_behavior(search_input, items); + if (has_sort) setup_sort_behavior(sort_buttons, items, item_parent); + } + + // Change number format AFTER parsing and storing the sort keys + apply_number_formatting(table_el); +} + +/** + * @param {HTMLInputElement} search_input + * @param {Array<{el: HTMLElement, sort_keys: Array<{num: number, str: string}>}>} items + */ +function setup_table_search_behavior(search_input, items) { + function onSearch() { + const lower_search = search_input.value + .toLowerCase() + .split(/\s+/) + .filter((s) => s); + for (const item of items) { + const show = lower_search.every((s) => + item.el.textContent.toLowerCase().includes(s), + ); + item.el.style.display = show ? "" : "none"; } + } + + search_input.addEventListener("input", onSearch); + onSearch(); } -function sqlpage_table(){ - // Tables - for (const r of document.querySelectorAll("[data-pre-init=table]")) { - new List(r, { - valueNames: [...r.getElementsByTagName("th")].map(t => t.textContent), - searchDelay: 100, - indexAsync: true +/**@param {HTMLElement} table_el */ +function apply_number_formatting(table_el) { + const header_els = table_el.querySelectorAll("thead > tr > th"); + const col_types = [...header_els].map((el) => el.dataset.column_type); + const col_rawnums = [...header_els].map((el) => !!el.dataset.raw_number); + const col_money = [...header_els].map((el) => !!el.dataset.money); + const number_format_locale = table_el.dataset.number_format_locale; + const number_format_digits = table_el.dataset.number_format_digits; + const currency = table_el.dataset.currency; + + for (const tr_el of table_el.querySelectorAll("tbody tr, tfoot tr")) { + const cells = tr_el.getElementsByTagName("td"); + for (let idx = 0; idx < cells.length; idx++) { + const column_type = col_types[idx]; + const is_raw_number = col_rawnums[idx]; + const cell_el = cells[idx]; + const text = cell_el.textContent; + + if (column_type === "number" && !is_raw_number && text) { + const num = Number.parseFloat(text); + const is_money = col_money[idx]; + cell_el.textContent = num.toLocaleString(number_format_locale, { + maximumFractionDigits: number_format_digits, + currency, + style: is_money ? "currency" : undefined, }); - r.removeAttribute("data-pre-init"); + } } + } } -function sqlpage_select_dropdown(){ - const selects = document.querySelectorAll("[data-pre-init=select-dropdown]"); - if (!selects.length) return; - const src = "https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.popular.min.js"; - if (!window.TomSelect) { - const script = document.createElement("script"); - script.src= src; - script.integrity = "sha384-aAqv9vleUwO75zAk1sGKd5VvRqXamBXwdxhtihEUPSeq1HtxwmZqQG/HxQnq7zaE"; - script.crossOrigin = "anonymous"; - script.nonce = nonce; - script.onload = sqlpage_select_dropdown; - document.head.appendChild(script); - return; - } - for (const s of selects) { - new TomSelect(s, { - create: s.dataset.create_new +/** Prepare the table rows for sorting. + * @param {HTMLElement} table_el + * @param {HTMLElement[]} sort_buttons + */ +function table_parse_data(table_el, sort_buttons) { + const is_num = [...sort_buttons].map( + (btn_el) => btn_el.parentElement.dataset.column_type === "number", + ); + return [...table_el.querySelectorAll("tbody tr")].map((tr_el) => { + const cells = tr_el.getElementsByTagName("td"); + return { + el: tr_el, + sort_keys: sort_buttons.map((_btn_el, idx) => { + const str = cells[idx]?.textContent; + const num = is_num[idx] ? Number.parseFloat(str) : Number.NaN; + return { num, str }; + }), + }; + }); +} + +/** + * Adds event listeners to the sort buttons to sort the table rows. + * @param {HTMLElement[]} sort_buttons + * @param {HTMLElement[]} items + * @param {HTMLElement} item_parent + */ +function setup_sort_behavior(sort_buttons, items, item_parent) { + sort_buttons.forEach((button, button_index) => { + button.addEventListener("click", function sort_items() { + const sort_desc = button.classList.contains("asc"); + for (const b of sort_buttons) { + b.classList.remove("asc", "desc"); + } + button.classList.add(sort_desc ? "desc" : "asc"); + const multiplier = sort_desc ? -1 : 1; + items.sort((a, b) => { + const a_key = a.sort_keys[button_index]; + const b_key = b.sort_keys[button_index]; + return ( + multiplier * + (Number.isNaN(a_key.num) || Number.isNaN(b_key.num) + ? a_key.str.localeCompare(b_key.str) + : a_key.num - b_key.num) + ); }); + item_parent.append(...items.map((item) => item.el)); + }); + }); +} + +function sqlpage_table() { + for (const r of document.querySelectorAll("[data-pre-init=table]")) { + r.removeAttribute("data-pre-init"); + setup_table(r); } } @@ -61,127 +157,162 @@ let is_leaflet_injected = false; let is_leaflet_loaded = false; function sqlpage_map() { - const first_map = document.querySelector("[data-pre-init=map]"); - if (first_map && !is_leaflet_injected) { - // Add the leaflet js and css to the page - const leaflet_css = document.createElement("link"); - leaflet_css.rel = "stylesheet"; - leaflet_css.href = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css"; - leaflet_css.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="; - leaflet_css.crossOrigin = "anonymous"; - document.head.appendChild(leaflet_css); - const leaflet_js = document.createElement("script"); - leaflet_js.src = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"; - leaflet_js.integrity = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="; - leaflet_js.crossOrigin = "anonymous"; - leaflet_js.nonce = nonce; - leaflet_js.onload = onLeafletLoad; - document.head.appendChild(leaflet_js); - is_leaflet_injected = true; - } - if (first_map && is_leaflet_loaded) { - onLeafletLoad(); - } - function parseCoords(coords) { - return coords && coords.split(",").map(c => parseFloat(c)); - } - function onLeafletLoad() { - is_leaflet_loaded = true; - const maps = document.querySelectorAll("[data-pre-init=map]"); - for (const m of maps) { - const tile_source = m.dataset.tile_source; - const maxZoom = +m.dataset.max_zoom; - const attribution = m.dataset.attribution; - const map = L.map(m, { attributionControl: !!attribution }); - const zoom = m.dataset.zoom; - let center = parseCoords(m.dataset.center); - if (tile_source) L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map); - map._sqlpage_markers = []; - for (const marker_elem of m.getElementsByClassName("marker")) { - setTimeout(addMarker, 0, marker_elem, map); - } - setTimeout(() => { - if (center == null && map._sqlpage_markers.length) { - map.fitBounds(map._sqlpage_markers.map(m => - m.getLatLng ? m.getLatLng() : m.getBounds() - )); - if (zoom != null) map.setZoom(+zoom); - } else map.setView(center, +zoom); - }, 100); - m.removeAttribute("data-pre-init"); - m.getElementsByClassName("spinner-border")[0]?.remove(); + const first_map = document.querySelector("[data-pre-init=map]"); + const leaflet_base_url = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4"; + if (first_map && !is_leaflet_injected) { + // Add the leaflet js and css to the page + const leaflet_css = document.createElement("link"); + leaflet_css.rel = "stylesheet"; + leaflet_css.href = `${leaflet_base_url}/dist/leaflet.css`; + leaflet_css.integrity = + "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="; + leaflet_css.crossOrigin = "anonymous"; + document.head.appendChild(leaflet_css); + const leaflet_js = document.createElement("script"); + leaflet_js.src = `${leaflet_base_url}/dist/leaflet.js`; + leaflet_js.integrity = + "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="; + leaflet_js.crossOrigin = "anonymous"; + leaflet_js.nonce = nonce; + leaflet_js.onload = onLeafletLoad; + document.head.appendChild(leaflet_js); + is_leaflet_injected = true; + } + if (first_map && is_leaflet_loaded) { + onLeafletLoad(); + } + /** + * + * @param {string|undefined} coords + * @returns {[number, number] | undefined} + */ + function parseCoords(coords) { + return coords?.split(",", 2).map((c) => Number.parseFloat(c)); + } + function onLeafletLoad() { + is_leaflet_loaded = true; + const maps = document.querySelectorAll("[data-pre-init=map]"); + for (const m of maps) { + const tile_source = m.dataset.tile_source; + const maxZoom = +m.dataset.max_zoom; + const attribution = m.dataset.attribution; + const map = L.map(m, { attributionControl: !!attribution }); + const zoom = m.dataset.zoom; + const center = parseCoords(m.dataset.center); + if (tile_source) + L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map); + map._sqlpage_markers = []; + for (const marker_elem of m.getElementsByClassName("marker")) { + setTimeout(addMarker, 0, marker_elem, map); } + setTimeout(() => { + if (center) map.setView(center, +zoom); + else { + const markerBounds = (m) => + m.getLatLng ? m.getLatLng() : m.getBounds(); + const bounds = map._sqlpage_markers.map(markerBounds); + if (bounds.length > 0) map.fitBounds(bounds); + else map.setView([51.505, 10], +zoom); + if (zoom != null) map.setZoom(+zoom); + } + }, 100); + m.removeAttribute("data-pre-init"); + m.getElementsByClassName("spinner-border")[0]?.remove(); } + } - function addMarker(marker_elem, map) { - const { dataset } = marker_elem; - const options = { - color: marker_elem.dataset.color, - title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(), - }; - const marker = - dataset.coords ? createMarker(marker_elem, options) - : createGeoJSONMarker(marker_elem, options); - marker.addTo(map); - map._sqlpage_markers.push(marker); - if (options.title) marker.bindPopup(marker_elem); - else if (marker_elem.dataset.link) marker.on('click', () => window.location = marker_elem.dataset.link); + function addMarker(marker_elem, map) { + const { dataset } = marker_elem; + const options = { + color: marker_elem.dataset.color, + title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(), + }; + const marker = dataset.coords + ? createMarker(marker_elem, options) + : createGeoJSONMarker(marker_elem, options); + marker.addTo(map); + map._sqlpage_markers.push(marker); + if (marker_elem.textContent.trim()) marker.bindPopup(marker_elem); + else if (marker_elem.dataset.link) { + marker.on("click", () => { + window.location.href = marker_elem.dataset.link; + }); } - function createMarker(marker_elem, options) { - const coords = parseCoords(marker_elem.dataset.coords); - const icon_obj = marker_elem.getElementsByClassName("mapicon")[0]; - if (icon_obj) { - const size = 1.5 * +(options.size || icon_obj.firstChild?.getAttribute('width') || 24); - options.icon = L.divIcon({ - html: icon_obj, - className: `border-0 bg-${options.color || 'primary'} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`, - iconSize: [size, size], - iconAnchor: [size/2, size/2], - }); - } - return L.marker(coords, options); + } + function createMarker(marker_elem, options) { + const coords = parseCoords(marker_elem.dataset.coords); + const icon_obj = marker_elem.getElementsByClassName("mapicon")[0]; + if (icon_obj) { + const size = + 1.5 * + +(options.size || icon_obj.firstChild?.getAttribute("width") || 24); + options.icon = L.divIcon({ + html: icon_obj, + className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`, + iconSize: [size, size], + iconAnchor: [size / 2, size / 2], + }); } - function createGeoJSONMarker(marker_elem, options) { - let geojson = JSON.parse(marker_elem.dataset.geojson); - if (options.color) { - options.color = get_tabler_color(options.color) || options.color; - } - function style({ properties }) { - if (typeof properties !== "object") return options; - return {...options, ...properties}; - } - function pointToLayer(feature, latlng) { - marker_elem.dataset.coords = latlng.lat + "," + latlng.lng; - return createMarker(marker_elem, { ...options, ...feature.properties }); - } - return L.geoJSON(geojson, { style, pointToLayer }); + return L.marker(coords, options); + } + function createGeoJSONMarker(marker_elem, options) { + const geojson = JSON.parse(marker_elem.dataset.geojson); + if (options.color) { + options.color = get_tabler_color(options.color) || options.color; } + function style({ properties }) { + if (typeof properties !== "object") return options; + return { ...options, ...properties }; + } + function pointToLayer(feature, latlng) { + marker_elem.dataset.coords = `${latlng.lat},${latlng.lng}`; + return createMarker(marker_elem, { ...options, ...feature.properties }); + } + return L.geoJSON(geojson, { style, pointToLayer }); + } } function sqlpage_form() { - const file_inputs = document.querySelectorAll("input[type=file][data-max-size]"); - for (const input of file_inputs) { - const max_size = +input.dataset.maxSize; - input.addEventListener("change", function() { - input.classList.remove("is-invalid"); - input.setCustomValidity(""); - for (const {size} of this.files) { - if (size > max_size){ - input.classList.add("is-invalid"); - return input.setCustomValidity(`File size must be less than ${max_size/1000} kB.`); - } + const file_inputs = document.querySelectorAll( + "input[type=file][data-max-size]", + ); + for (const input of file_inputs) { + const max_size = +input.dataset.maxSize; + input.addEventListener("change", function () { + input.classList.remove("is-invalid"); + input.setCustomValidity(""); + for (const { size } of this.files) { + if (size > max_size) { + input.classList.add("is-invalid"); + return input.setCustomValidity( + `File size must be less than ${max_size / 1000} kB.`, + ); } - }); - } + } + }); + } + + const auto_submit_forms = document.querySelectorAll("form[data-auto-submit]"); + for (const form of auto_submit_forms) { + form.addEventListener("change", () => form.submit()); + } } function get_tabler_color(name) { - return getComputedStyle(document.documentElement).getPropertyValue('--tblr-' + name); + return getComputedStyle(document.documentElement).getPropertyValue( + `--tblr-${name}`, + ); } function load_scripts() { - let addjs = document.querySelectorAll("[data-sqlpage-js]"); - for (const js of new Set([...addjs].map(({dataset}) => dataset.sqlpageJs))) { + const addjs = document.querySelectorAll("[data-sqlpage-js]"); + const existing_scripts = new Set( + [...document.querySelectorAll("script")].map((s) => s.src), + ); + for (const el of addjs) { + const js = new URL(el.dataset.sqlpageJs, window.location.href).href; + if (existing_scripts.has(js)) continue; + existing_scripts.add(js); const script = document.createElement("script"); script.src = js; document.head.appendChild(script); @@ -189,14 +320,48 @@ function load_scripts() { } function add_init_fn(f) { - document.addEventListener('DOMContentLoaded', f); - document.addEventListener('fragment-loaded', f); + document.addEventListener("DOMContentLoaded", f); + document.addEventListener("fragment-loaded", f); if (document.readyState !== "loading") setTimeout(f, 0); } - add_init_fn(sqlpage_table); add_init_fn(sqlpage_map); add_init_fn(sqlpage_card); add_init_fn(sqlpage_form); -add_init_fn(load_scripts); \ No newline at end of file +add_init_fn(load_scripts); + +function init_bootstrap_components(event) { + const bootstrap = window.bootstrap || window.tabler.bootstrap; + const fragment = event.target; + for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) { + new bootstrap.Tooltip(el); + } + for (const el of fragment.querySelectorAll('[data-bs-toggle="popover"]')) { + new bootstrap.Popover(el); + } + for (const el of fragment.querySelectorAll('[data-bs-toggle="dropdown"]')) { + new bootstrap.Dropdown(el); + } + for (const el of fragment.querySelectorAll('[data-bs-ride="carousel"]')) { + new bootstrap.Carousel(el); + } +} + +document.addEventListener("fragment-loaded", init_bootstrap_components); + +function open_modal_for_hash() { + const hash = window.location.hash.substring(1); + if (!hash) return; + const modal = document.getElementById(hash); + if (!modal || !modal.classList.contains("modal")) return; + const bootstrap_modal = + window.tabler.bootstrap.Modal.getOrCreateInstance(modal); + bootstrap_modal.show(); + modal.addEventListener("hidden.bs.modal", () => { + window.history.replaceState(null, "", "#"); + }); +} + +window.addEventListener("hashchange", open_modal_for_hash); +window.addEventListener("DOMContentLoaded", open_modal_for_hash); diff --git a/sqlpage/tabler-icons.svg b/sqlpage/tabler-icons.svg index eccb1104..f63a53f4 100644 --- a/sqlpage/tabler-icons.svg +++ b/sqlpage/tabler-icons.svg @@ -1 +1 @@ -/* !include https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.10.0/dist/tabler-sprite.svg */ \ No newline at end of file +/* !include https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.34.0/dist/tabler-sprite.svg */ diff --git a/sqlpage/templates/README.md b/sqlpage/templates/README.md index b1521b0a..c70a3ace 100644 --- a/sqlpage/templates/README.md +++ b/sqlpage/templates/README.md @@ -7,14 +7,14 @@ SQLPage templates are handlebars[^1] files that are used to render the results o ## Default components SQLPage comes with a set of default[^2] components that you can use without having to write any code. -These are documented on https://sql.ophir.dev/components.sql +These are documented on https://sql-page.com/components.sql ## Custom components -You can [write your own component templates](https://sql.ophir.dev/custom_components.sql) +You can [write your own component templates](https://sql-page.com/custom_components.sql) and place them in the `sqlpage/templates` directory. To override a default component, create a file with the same name as the default component. If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory in the SQLPage source code[^2]. -[^2]: A simple component to start from: https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file +[^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars \ No newline at end of file diff --git a/sqlpage/templates/alert.handlebars b/sqlpage/templates/alert.handlebars index 1d261e04..cf39f132 100644 --- a/sqlpage/templates/alert.handlebars +++ b/sqlpage/templates/alert.handlebars @@ -1,48 +1,41 @@ -

-
+
{{~#if description}}

{{description}}

{{/if~}} {{~#if description_md~}} {{{markdown description_md}}} diff --git a/sqlpage/templates/modal.handlebars b/sqlpage/templates/modal.handlebars new file mode 100644 index 00000000..8b13ba2e --- /dev/null +++ b/sqlpage/templates/modal.handlebars @@ -0,0 +1,53 @@ + diff --git a/sqlpage/templates/pagination.handlebars b/sqlpage/templates/pagination.handlebars new file mode 100644 index 00000000..c3cf29a4 --- /dev/null +++ b/sqlpage/templates/pagination.handlebars @@ -0,0 +1,51 @@ +
+
+ +
+
diff --git a/sqlpage/templates/shell-empty.handlebars b/sqlpage/templates/shell-empty.handlebars index f71ccfc0..5614a830 100644 --- a/sqlpage/templates/shell-empty.handlebars +++ b/sqlpage/templates/shell-empty.handlebars @@ -1,2 +1,3 @@ {{{~html~}}} +{{{~contents~}}} {{~#each_row~}}{{~/each_row~}} diff --git a/sqlpage/templates/shell.handlebars b/sqlpage/templates/shell.handlebars index c8a21d2d..0e1c7a8e 100644 --- a/sqlpage/templates/shell.handlebars +++ b/sqlpage/templates/shell.handlebars @@ -1,5 +1,10 @@ - + {{default title "SQLPage"}} @@ -23,7 +28,6 @@ font-weight: normal; font-style: normal; } - :root { --tblr-font-sans-serif: 'LocalFont', Arial, sans-serif; } @@ -53,8 +57,20 @@ {{/if}} {{/each}} + - + {{#if title}} + + {{/if}} + {{#if description}} + + + {{/if}} + {{#if preview_image}} + + + {{/if}} + {{#if norobot}} {{/if}} @@ -65,7 +81,7 @@ {{#if rss}} {{/if}} - + {{#if social_image}} {{/if}} @@ -78,9 +94,15 @@ {{~#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}} {{~#with (parse_json this)}} {{#if (or (or this.title this.icon) this.image)}} -