chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Waiting to run
CI / test (, sqlite, sqlite::memory:) (push) Blocked by required conditions
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Blocked by required conditions
CI / windows_test (push) Waiting to run
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Blocked by required conditions
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Blocked by required conditions
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Blocked by required conditions
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Blocked by required conditions
CI / playwright (push) Blocked by required conditions
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Waiting to run
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Waiting to run
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Waiting to run
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Waiting to run
CI / docker_build (linux/amd64, minimal) (push) Waiting to run
CI / hurl_examples (push) Waiting to run
CI / hurl (${{ matrix.example }}) (push) Blocked by required conditions
CI / docker_push (duckdb) (push) Blocked by required conditions
CI / docker_push (minimal) (push) Blocked by required conditions
deploy website / deploy_official_site (push) Waiting to run
@@ -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 = []
|
||||
@@ -0,0 +1,15 @@
|
||||
/target
|
||||
.git/
|
||||
sqlpage.db
|
||||
docs/
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
configuration.md
|
||||
*Dockerfile
|
||||
.dockerignore
|
||||
.github/
|
||||
examples/
|
||||
mssql/
|
||||
tests/
|
||||
.idea/
|
||||
@@ -0,0 +1,2 @@
|
||||
# Set COMPOSE_PROFILES to one of the following: postgres, mysql, mssql. If set to mssql, you will need to change the depends_on propery in docker-compose.yml
|
||||
COMPOSE_PROFILES=mariadb
|
||||
@@ -0,0 +1 @@
|
||||
*.sql linguist-detectable=true
|
||||
@@ -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']
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
|
||||
List of steps to reproduce the behavior. Include the sql file you are using and the eventual relevant parts of your database schema
|
||||
|
||||
```sql
|
||||
-- your sql here
|
||||
```
|
||||
|
||||
### Actual behavior
|
||||
|
||||
After following these steps, what happened ?
|
||||
If you saw an error on the command line or inside your page, then paste it here
|
||||
|
||||
```
|
||||
your error message here
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Version information
|
||||
|
||||
- OS:
|
||||
- Database [e.g. SQLite, Postgres]
|
||||
- SQLPage Version [found when hovering the default footer of pages]:
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
What are you building with SQLPage ?
|
||||
|
||||
(If you are not an actual user of SQLPage, then you can open a discussion thread in the discussion section instead of an issue here.)
|
||||
|
||||
What is your problem ? A description of the problem, not the solution you are proposing.
|
||||
|
||||
What are you currently doing ? Since your solution is not implemented in SQLPage currently, what are you doing instead ?
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,539 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
branches:
|
||||
- "main"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/official-site.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/official-site.yml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
HURL_VERSION: 8.0.0
|
||||
REGISTRY_USERNAME: lovasoa
|
||||
REGISTRY_IMAGE: lovasoa/sqlpage
|
||||
|
||||
jobs:
|
||||
compile_and_lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
with:
|
||||
shared-key: rust-sqlpage-proj-test
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
# The database matrix below runs the same Rust test harnesses against
|
||||
# different DATABASE_URL values. Package the Linux test executables once
|
||||
# here so those jobs do not recompile SQLPage or its dependencies.
|
||||
- name: Package Linux Rust test binaries
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf target/sqlpage-test-binaries
|
||||
mkdir -p target/sqlpage-test-binaries
|
||||
cargo test --features odbc-static --no-run --message-format=json \
|
||||
| jq -r 'select(.profile.test == true and .executable != null) | .executable' \
|
||||
| while IFS= read -r test_binary; do
|
||||
cp -- "$test_binary" target/sqlpage-test-binaries/
|
||||
done
|
||||
test -n "$(find target/sqlpage-test-binaries -maxdepth 1 -type f -print -quit)"
|
||||
tar -C target/sqlpage-test-binaries -czf target/sqlpage-linux-test-binaries.tar.gz .
|
||||
- name: Build Linux binary
|
||||
run: cargo build --features odbc-static
|
||||
- name: Upload Linux Rust test binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sqlpage-linux-test-binaries
|
||||
path: "target/sqlpage-linux-test-binaries.tar.gz"
|
||||
- name: Upload Linux binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sqlpage-linux-debug
|
||||
path: "target/debug/sqlpage"
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile_and_lint
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- database: sqlite
|
||||
container: ""
|
||||
db_url: "sqlite::memory:"
|
||||
- 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
|
||||
- database: oracle
|
||||
container: oracle
|
||||
db_url: "Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
# Reuse the exact Linux test harnesses produced by compile_and_lint.
|
||||
# This keeps the DB matrix focused on database behavior instead of
|
||||
# compiling the same Rust crate five more times.
|
||||
- name: Download Linux Rust test binaries
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sqlpage-linux-test-binaries
|
||||
path: target
|
||||
- name: Extract Linux Rust test binaries
|
||||
run: |
|
||||
mkdir -p target/sqlpage-test-binaries
|
||||
tar -xzf target/sqlpage-linux-test-binaries.tar.gz -C target/sqlpage-test-binaries
|
||||
- name: Install PostgreSQL ODBC driver
|
||||
if: matrix.setup_odbc
|
||||
run: sudo apt-get install -y odbc-postgresql
|
||||
- name: Install Oracle ODBC driver
|
||||
if: matrix.database == 'oracle'
|
||||
run: |
|
||||
sudo apt-get install -y alien libaio1t64 libodbcinst2 unixodbc
|
||||
sudo rpm --import https://yum.oracle.com/RPM-GPG-KEY-oracle-ol8
|
||||
wget https://yum.oracle.com/repo/OracleLinux/OL8/oracle/instantclient21/x86_64/getPackage/oracle-instantclient-{basic,odbc}-21.21.0.0.0-1.el8.x86_64.rpm
|
||||
sudo alien -i oracle-instantclient-basic-21.21.0.0.0-1.el8.x86_64.rpm
|
||||
sudo alien -i oracle-instantclient-odbc-21.21.0.0.0-1.el8.x86_64.rpm
|
||||
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1
|
||||
sudo /usr/lib/oracle/21/client64/bin/odbc_update_ini.sh / /usr/lib/oracle/21/client64/lib
|
||||
echo "LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV"
|
||||
- name: Start database container
|
||||
if: matrix.container != ''
|
||||
run: docker compose up --wait ${{ matrix.container }}
|
||||
- name: Show container logs
|
||||
if: failure() && matrix.container != ''
|
||||
run: docker compose logs ${{ matrix.container }}
|
||||
- name: Run tests against ${{ matrix.database }}
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
test_binaries=(target/sqlpage-test-binaries/*)
|
||||
if ((${#test_binaries[@]} == 0)); then
|
||||
echo "No test binaries were found in target/sqlpage-test-binaries" >&2
|
||||
exit 1
|
||||
fi
|
||||
for test_binary in "${test_binaries[@]}"; do
|
||||
echo "::group::$(basename "$test_binary")"
|
||||
"$test_binary"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
env:
|
||||
DATABASE_URL: ${{ matrix.db_url }}
|
||||
MALLOC_CHECK_: 3
|
||||
MALLOC_PERTURB_: 10
|
||||
|
||||
windows_test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
- name: Set up Windows incremental cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
target/debug/.fingerprint/sqlpage-*
|
||||
target/debug/build/sqlpage-*
|
||||
target/debug/deps/libsqlpage-*.rlib
|
||||
target/debug/deps/libsqlpage-*.rmeta
|
||||
target/debug/deps/sqlpage-*.d
|
||||
target/debug/incremental/sqlpage-*
|
||||
key: windows-incremental-${{ github.event.pull_request.number || github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
windows-incremental-${{ github.event.pull_request.number || github.ref_name }}-
|
||||
- run: cargo test
|
||||
env:
|
||||
CARGO_INCREMENTAL: 1
|
||||
RUST_BACKTRACE: 1
|
||||
- name: Upload Windows binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sqlpage-windows-debug
|
||||
path: "target/debug/sqlpage.exe"
|
||||
|
||||
playwright:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: compile_and_lint
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tests/end-to-end
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
cache-dependency-path: ./tests/end-to-end/package-lock.json
|
||||
- run: sudo apt-get update && sudo apt-get install -y unixodbc-dev
|
||||
- run: npm ci && npx playwright install --with-deps chromium
|
||||
# The browser tests exercise the official site, but they do not need a
|
||||
# separate Rust build. Reuse the binary compiled and tested above.
|
||||
- name: Download Linux binary
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sqlpage-linux-debug
|
||||
path: ${{ runner.temp }}/sqlpage-bin
|
||||
- name: Start official site and wait for it to be ready
|
||||
timeout-minutes: 1
|
||||
run: |
|
||||
chmod +x "${{ runner.temp }}/sqlpage-bin/sqlpage"
|
||||
"${{ runner.temp }}/sqlpage-bin/sqlpage" 2>/tmp/stderrlog &
|
||||
tail -f /tmp/stderrlog | grep -q "started successfully"
|
||||
working-directory: ./examples/official-site
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- name: show server logs
|
||||
if: failure()
|
||||
run: cat /tmp/stderrlog
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./tests/end-to-end/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
docker_build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/arm/v7
|
||||
variant: minimal
|
||||
tag_suffix: -linux-arm-v7
|
||||
- platform: linux/arm64
|
||||
variant: minimal
|
||||
tag_suffix: -linux-arm64
|
||||
- platform: linux/amd64
|
||||
variant: duckdb
|
||||
tag_suffix: -linux-amd64-duckdb
|
||||
- platform: linux/arm64
|
||||
variant: duckdb
|
||||
tag_suffix: -linux-arm64-duckdb
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- id: cache-scope
|
||||
name: Docker cache scope
|
||||
run: |
|
||||
ref_scope="main"
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
ref_scope="pr-${{ github.event.pull_request.number }}"
|
||||
fi
|
||||
|
||||
recipe_hash="${{ hashFiles('Dockerfile', '.cargo/**', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'scripts/**', 'sqlpage/**') }}"
|
||||
{
|
||||
echo "current=sqlpage-${ref_scope}${{ matrix.tag_suffix }}-${recipe_hash}"
|
||||
echo "main=sqlpage-main${{ matrix.tag_suffix }}-${recipe_hash}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
flavor: suffix=${{ matrix.tag_suffix }}
|
||||
labels: |
|
||||
org.opencontainers.image.created=1970-01-01T00:00:00Z
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ env.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
# Use BuildKit's Git context instead of the mutable runner workspace.
|
||||
# The dependency cache should be keyed by committed source, not by a
|
||||
# per-job local context stream.
|
||||
context: "{{defaultContext}}"
|
||||
platforms: ${{ matrix.platform }}
|
||||
target: ${{ matrix.variant }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.current }}
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.main }}
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:main${{ matrix.tag_suffix }}
|
||||
cache-to: type=gha,scope=${{ steps.cache-scope.outputs.current }},mode=max
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v7
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: digests-${{ matrix.variant }}${{ matrix.tag_suffix }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
docker_build_amd64_minimal:
|
||||
name: docker_build (linux/amd64, minimal)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
flavor: suffix=-linux-amd64
|
||||
labels: |
|
||||
org.opencontainers.image.created=1970-01-01T00:00:00Z
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- id: cache-scope
|
||||
name: Docker cache scope
|
||||
run: |
|
||||
ref_scope="main"
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
ref_scope="pr-${{ github.event.pull_request.number }}"
|
||||
fi
|
||||
|
||||
recipe_hash="${{ hashFiles('Dockerfile', '.cargo/**', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'scripts/**', 'sqlpage/**') }}"
|
||||
{
|
||||
echo "artifact=sqlpage-${ref_scope}-linux-amd64-hurl-${recipe_hash}"
|
||||
echo "current=sqlpage-${ref_scope}-linux-amd64-${recipe_hash}"
|
||||
echo "main=sqlpage-main-linux-amd64-${recipe_hash}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ env.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build image for Hurl examples
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
# Use BuildKit's Git context instead of the mutable runner workspace.
|
||||
# The dependency cache should be keyed by committed source, not by a
|
||||
# per-job local context stream.
|
||||
context: "{{defaultContext}}"
|
||||
platforms: linux/amd64
|
||||
target: minimal
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}
|
||||
${{ env.REGISTRY_IMAGE }}:main
|
||||
outputs: type=docker,dest=${{ runner.temp }}/sqlpage.tar
|
||||
cache-from: |
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.artifact }}
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.current }}
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.main }}
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:main-linux-amd64
|
||||
cache-to: type=gha,scope=${{ steps.cache-scope.outputs.artifact }},mode=max
|
||||
- name: Upload SQLPage image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sqlpage-linux-amd64-minimal-image
|
||||
path: ${{ runner.temp }}/sqlpage.tar
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
# Use BuildKit's Git context instead of the mutable runner workspace.
|
||||
# The dependency cache should be keyed by committed source, not by a
|
||||
# per-job local context stream.
|
||||
context: "{{defaultContext}}"
|
||||
platforms: linux/amd64
|
||||
target: minimal
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.current }}
|
||||
type=gha,scope=${{ steps.cache-scope.outputs.main }}
|
||||
type=registry,ref=${{ env.REGISTRY_IMAGE }}:main-linux-amd64
|
||||
cache-to: type=gha,scope=${{ steps.cache-scope.outputs.current }},mode=max
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v7
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: digests-minimal-linux-amd64
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
hurl_examples:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
examples: ${{ steps.examples.outputs.examples }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: examples
|
||||
run: |
|
||||
examples="$(find examples -mindepth 2 -maxdepth 2 -name test.hurl -print | sed 's#/test.hurl$##' | sort | jq -R -s -c 'split("\n")[:-1]')"
|
||||
echo "examples=$examples" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore Hurl archive
|
||||
id: hurl-cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .cache/hurl
|
||||
key: hurl-${{ runner.os }}-x86_64-unknown-linux-gnu-${{ env.HURL_VERSION }}
|
||||
- name: Download Hurl archive
|
||||
if: steps.hurl-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive="hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
base_url="https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}"
|
||||
mkdir -p .cache/hurl
|
||||
curl --fail --location --retry 5 --retry-delay 2 --output ".cache/hurl/${archive}" "${base_url}/${archive}"
|
||||
curl --fail --location --retry 5 --retry-delay 2 --output ".cache/hurl/${archive}.sha256" "${base_url}/${archive}.sha256"
|
||||
(
|
||||
cd .cache/hurl
|
||||
expected_sha="$(tr -d '[:space:]' < "${archive}.sha256")"
|
||||
actual_sha="$(sha256sum "$archive" | cut -d ' ' -f 1)"
|
||||
test "$expected_sha" = "$actual_sha"
|
||||
)
|
||||
- name: Upload Hurl archive
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: hurl-${{ env.HURL_VERSION }}-x86_64-unknown-linux-gnu
|
||||
path: .cache/hurl/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
hurl:
|
||||
name: hurl (${{ matrix.example }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs:
|
||||
- docker_build_amd64_minimal
|
||||
- hurl_examples
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example: ${{ fromJSON(needs.hurl_examples.outputs.examples) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download Hurl archive
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: hurl-${{ env.HURL_VERSION }}-x86_64-unknown-linux-gnu
|
||||
path: ${{ runner.temp }}/hurl
|
||||
- name: Install Hurl
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive="hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
(
|
||||
cd "$RUNNER_TEMP/hurl"
|
||||
expected_sha="$(tr -d '[:space:]' < "${archive}.sha256")"
|
||||
actual_sha="$(sha256sum "$archive" | cut -d ' ' -f 1)"
|
||||
test "$expected_sha" = "$actual_sha"
|
||||
tar xzf "$archive" -C "$RUNNER_TEMP/hurl"
|
||||
)
|
||||
echo "$RUNNER_TEMP/hurl/hurl-${HURL_VERSION}-x86_64-unknown-linux-gnu/bin" >> "$GITHUB_PATH"
|
||||
- name: Download SQLPage image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: sqlpage-linux-amd64-minimal-image
|
||||
path: ${{ runner.temp }}/sqlpage-image
|
||||
- name: Load SQLPage image
|
||||
run: |
|
||||
docker load --input "${{ runner.temp }}/sqlpage-image/sqlpage.tar"
|
||||
docker tag "${{ env.REGISTRY_IMAGE }}:main" "${{ env.REGISTRY_IMAGE }}:latest"
|
||||
- name: Run example Hurl test
|
||||
run: scripts/test-examples-hurl.sh "${{ matrix.example }}"
|
||||
|
||||
docker_push:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
needs:
|
||||
- docker_build_amd64_minimal
|
||||
- docker_build
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- minimal
|
||||
- duckdb
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v8
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
with:
|
||||
pattern: digests-${{ matrix.variant }}*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ env.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
env:
|
||||
TAG_SUFFIX: ${{ matrix.variant != 'minimal' && format('-{0}', matrix.variant) || '' }}
|
||||
run: |
|
||||
tags=(-t "${REGISTRY_IMAGE}:${GITHUB_REF_NAME}${TAG_SUFFIX}")
|
||||
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
|
||||
tags+=(-t "${REGISTRY_IMAGE}:latest${TAG_SUFFIX}")
|
||||
fi
|
||||
shopt -s nullglob
|
||||
sources=()
|
||||
for digest in *; do
|
||||
sources+=("${REGISTRY_IMAGE}@sha256:${digest}")
|
||||
done
|
||||
((${#sources[@]} > 0))
|
||||
docker buildx imagetools create "${tags[@]}" "${sources[@]}"
|
||||
- name: Inspect image
|
||||
env:
|
||||
TAG_SUFFIX: ${{ matrix.variant != 'minimal' && format('-{0}', matrix.variant) || '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect "${REGISTRY_IMAGE}:${GITHUB_REF_NAME}${TAG_SUFFIX}"
|
||||
@@ -0,0 +1,39 @@
|
||||
name: "deploy website"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: site-deploy
|
||||
|
||||
jobs:
|
||||
deploy_official_site:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cloning repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: get_gitsha
|
||||
name: Make a commit for the official site
|
||||
run: |
|
||||
cp -r examples/official-site /tmp/
|
||||
rm -rf * .[!.]*
|
||||
cp -r /tmp/official-site/* .
|
||||
git init .
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
git add .
|
||||
git commit -am "site update $(date)"
|
||||
echo "gitsha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Push to dokku
|
||||
uses: dokku/github-action@master
|
||||
with:
|
||||
git_remote_url: "dokku@${{ secrets.DEPLOY_IP }}:sqlpage"
|
||||
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
ssh_host_key: ${{ secrets.SSH_HOST_KEY }}
|
||||
git_push_flags: "--force"
|
||||
branch: "main"
|
||||
ci_commit: ${{ steps.get_gitsha.outputs.gitsha }}
|
||||
@@ -0,0 +1,174 @@
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
# 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-macos-windows:
|
||||
name: Build sqlpage binaries (macOS & Windows)
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest]
|
||||
include:
|
||||
- os: windows-latest
|
||||
binary_extension: .exe
|
||||
target: x86_64-pc-windows-msvc
|
||||
features: ""
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
features: "odbc-static"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
- 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@v7
|
||||
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@v2.2
|
||||
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@v7
|
||||
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@v7
|
||||
with:
|
||||
name: sqlpage ${{ matrix.os }}
|
||||
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@v6
|
||||
- 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@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
- name: Build
|
||||
run: cargo build --profile superoptimized --locked --target x86_64-unknown-linux-gnu --features "odbc-static"
|
||||
- uses: actions/upload-artifact@v7
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- run: docker build -t sqlpage-lambda-builder . -f lambda.Dockerfile --target builder
|
||||
- run: docker run sqlpage-lambda-builder cat deploy.zip > sqlpage-aws-lambda.zip
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sqlpage aws lambda serverless image
|
||||
path: sqlpage-aws-lambda.zip
|
||||
|
||||
create_release:
|
||||
name: Create Github Release
|
||||
needs: [build-macos-windows, build-linux, build-aws]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v8
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
- run: |
|
||||
rm -rf sqlpage/templates/*.handlebars;
|
||||
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-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@v3.0.0
|
||||
with:
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, 'beta') }}
|
||||
files: |
|
||||
sqlpage-windows.zip
|
||||
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@v6
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||
env:
|
||||
NODE_OPTIONS: --no-deprecation
|
||||
- run: sudo apt-get update && sudo apt-get install -y unixodbc-dev
|
||||
- name: Publish to crates.io
|
||||
run: |
|
||||
set +e
|
||||
cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} 2>&1 | tee cargo-publish.log
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if grep -Eiq 'already (uploaded|exists)|version .* is already' cargo-publish.log; then
|
||||
echo "sqlpage ${GITHUB_REF_NAME#v} is already published on crates.io"
|
||||
else
|
||||
exit "$status"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
.DS_STORE
|
||||
/target
|
||||
sqlpage.db
|
||||
.idea/
|
||||
*.mm_profdata
|
||||
docs/presentation-pgconf.html
|
||||
examples/inrap_badass/
|
||||
sqlpage/https/*
|
||||
x.sql
|
||||
xbed.sql
|
||||
**/sqlpage.bin
|
||||
node_modules/
|
||||
sqlpage/sqlpage.db
|
||||
tests_uploads/
|
||||
@@ -0,0 +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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": ["./Cargo.toml"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Project-local debug tasks
|
||||
//
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[
|
||||
{
|
||||
"label": "Debug with DuckDB",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": ["build"],
|
||||
},
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/sqlpage",
|
||||
"env": {
|
||||
"DATABASE_URL": "DSN=DuckDB",
|
||||
},
|
||||
// sourceLanguages is required for CodeLLDB (not GDB) when using Rust
|
||||
"sourceLanguages": ["rust"],
|
||||
"request": "launch",
|
||||
"adapter": "CodeLLDB",
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
# SQLPage architecture
|
||||
|
||||
SQLPage is an SQL-only web application builder and web server. An application is primarily a set of `.sql`
|
||||
files: SQLPage routes an HTTP request to a file, executes its statements against a database, interprets rows
|
||||
whose `component` column names a UI component, and streams the resulting HTML (or another response) to the
|
||||
client. It is intended for fast, data-centric applications while still allowing custom HTML, CSS, and
|
||||
JavaScript where needed.
|
||||
|
||||
## Features and repository layout
|
||||
|
||||
- **Application entry point and configuration** (`src/main.rs`, `src/lib.rs`, `src/app_config.rs`, `src/cli/`).
|
||||
The executable starts the server; application state, configuration, environment variables, and command-line
|
||||
handling are defined here. `configuration.md` documents the user-facing settings.
|
||||
- **SQL semantics and execution** (`src/webserver/database/`). SQLPage uses the database's SQL for selects, joins, aggregation, inserts, updates,
|
||||
deletes, transactions, JSON processing, and database-specific features. It parses SQL, recognizes SQLPage
|
||||
extensions, binds request values safely, and sends ordinary SQL to the selected database. SQL files contain
|
||||
sequential statements; result sets become component invocations in response order. `SET` assigns a value
|
||||
to a mutable SQLPage variable and is useful for reusing query results or controlling later statements.
|
||||
- **Request variables** (`src/webserver/request_variables.rs`, `src/webserver/http_request_info.rs`,
|
||||
`src/webserver/database/syntax_tree.rs`). `?name` refers to a URL/GET parameter, `:name` explicitly refers to a form/POST
|
||||
value, and `$name` is the compatibility shorthand that uses a POST value when present and otherwise a GET
|
||||
value (a SET variable takes precedence where applicable). Values are passed as parameters, not interpolated
|
||||
into SQL. GET and POST variables are request inputs; SET variables are mutable during request execution.
|
||||
`sqlpage.variables()` exposes them as JSON, with SET > POST > GET precedence.
|
||||
- **SQLPage functions** (`src/webserver/database/sqlpage_functions/`). Calls such as `sqlpage.fetch`, `sqlpage.run_sql`, `sqlpage.set_variable`, file
|
||||
readers, hashing/HMAC helpers, request metadata, uploads, headers/cookies, URL helpers, OIDC user info,
|
||||
and HTTP fetch are registered in `src/webserver/database/sqlpage_functions/functions.rs`. Functions can
|
||||
return values, alter response/request state, include another SQL file, or raise an error. `sqlpage.exec`
|
||||
is deliberately disabled by default because it runs server processes.
|
||||
- **Database support and pooling** (`src/webserver/database/connect.rs`, `execute_queries.rs`, `migrations.rs`).
|
||||
Native drivers support SQLite, PostgreSQL, MySQL, and Microsoft SQL
|
||||
Server; the ODBC driver provides access to other ODBC-compatible databases. SQLPage uses `sqlx` and a
|
||||
reusable connection pool, with configurable maximum connections, idle/lifetime timeouts, acquire timeout,
|
||||
retries, and optional `on_connect.sql`/`on_reset.sql` hooks. Database-specific SQL should be isolated or
|
||||
covered by the relevant database tests.
|
||||
- **Rendering and components** (`src/render.rs`, `src/templates.rs`, `src/dynamic_component.rs`,
|
||||
`src/template_helpers.rs`, `sqlpage/templates/`, `sqlpage/sqlpage.css`, `sqlpage/sqlpage.js`). Built-in components live in `sqlpage/templates/*.handlebars` and cover shells, text,
|
||||
tables, lists, cards, charts, forms, navigation, modals, downloads, maps, and more. Query columns map to
|
||||
component properties; nested/dynamic components and `sqlpage.run_sql` support composition and lazy loading.
|
||||
Custom Handlebars components can be placed in the configured `sqlpage/templates` directory. Raw HTML and
|
||||
custom assets are possible through the HTML/shell components. Rendering is streamed so the response can
|
||||
start while later query results are still being processed.
|
||||
- **Control flow and errors** (`src/webserver/error.rs`, `error_with_status.rs`, `routing.rs`,
|
||||
`src/default_404.sql`). SQL remains declarative: use predicates, `CASE`, `SET`, component rows, and
|
||||
the `redirect` component to conditionally continue, redirect, or implement guards/error pages. There is no
|
||||
general SQLPage `IF` statement. Parse, database, function, component, and response errors are converted to
|
||||
contextual HTTP errors; `default_404.sql` handles missing routes. Do not hide errors by changing unrelated
|
||||
error handling or tests.
|
||||
- **HTTP server and client** (`src/webserver/http.rs`, `http_client.rs`, `response_writer.rs`, `static_content.rs`,
|
||||
`https.rs`, `content_security_policy.rs`, `server_timing.rs`). The server is built on Actix Web, supports normal HTTP request/response
|
||||
handling, streaming, uploads, static assets, HTTP/2, HTTPS, and optional Unix sockets/serverless adapters.
|
||||
The shared outbound client is used by HTTP-fetch and OIDC integrations and honors configured/native TLS
|
||||
certificates and timeouts. Content-security-policy and response/header helpers are part of the request
|
||||
pipeline.
|
||||
- **OIDC** (`src/webserver/oidc.rs`, `src/webserver/database/sqlpage_functions/functions/user_info.rs`).
|
||||
Optional OpenID Connect middleware protects configured path prefixes, performs provider discovery,
|
||||
login/callback/logout, validates tokens, maintains an authenticated cookie, and exposes identity claims to
|
||||
SQLPage functions. Configuration is in `AppConfig`/`configuration.md`; public paths can be excluded.
|
||||
- **Caching and files** (`src/filesystem.rs`, `src/file_cache.rs`, `src/telemetry*.rs`). Parsed SQL files are cached. Files may come from the web root/filesystem or from the
|
||||
database-backed `sqlpage_files` store, and templates/migrations are loaded from the configuration directory.
|
||||
Telemetry, request timing, and debug logging help diagnose query, pool, and rendering performance.
|
||||
|
||||
- **Examples, tests, and project operations** (`examples/`, `tests/`, `configuration.md`, `CONTRIBUTING.md`,
|
||||
`README.md`, `.github/workflows/ci.yml`). Examples include the official documentation site and its migrations;
|
||||
tests cover SQL fixtures, database variants, uploads, OIDC, and server timing. The contribution guide and CI
|
||||
workflow define the development and validation conventions.
|
||||
- **Deployment and local infrastructure** (`Dockerfile`, `lambda.Dockerfile`, `docker-compose.yml`,
|
||||
`sqlpage.service`). These provide container, serverless, local database-testing, and service deployment support.
|
||||
|
||||
## Documentation and release notes
|
||||
|
||||
The official documentation site is itself an SQLPage application in `examples/official-site/`. Its database
|
||||
schema and documentation content are created by the SQL migrations in
|
||||
`examples/official-site/sqlpage/migrations/`; the site is recreated from scratch during deployment. Existing
|
||||
official-site migrations are editable source files: update the migration that already documents a component,
|
||||
function, configuration option, or feature in place. Do not create a new migration merely to update existing
|
||||
documentation. Add a new migration only for genuinely new documentation content when that is the established
|
||||
pattern for the relevant area.
|
||||
|
||||
- Add or update a component's row, parameters, and examples in the component documentation migrations when
|
||||
changing `sqlpage/templates/` or component behavior.
|
||||
- Add or update a function's description, parameters, examples, and caveats in the function documentation
|
||||
migrations when changing `sqlpage_functions` or any `sqlpage.*` function behavior.
|
||||
- Update the relevant configuration documentation when adding or changing `AppConfig` settings, environment
|
||||
variables, defaults, authentication behavior, HTTP/TLS behavior, database settings, or custom components.
|
||||
- Document OIDC changes in the authentication/OIDC migrations, including configuration requirements, exposed
|
||||
claims/functions, login/logout behavior, and security implications.
|
||||
- Document other user-visible behavior—SQL syntax extensions, variables, control flow, errors, uploads,
|
||||
rendering, HTTP endpoints, performance, or deployment—in the corresponding official-site SQL page or
|
||||
migration. Follow nearby migrations and keep examples executable and database-portable where possible.
|
||||
- Update `CHANGELOG.md` for user-visible changes, bug fixes, breaking changes, deprecations, and noteworthy
|
||||
internal changes. Keep the entry concise and use the existing version/section conventions.
|
||||
|
||||
## 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)
|
||||
@@ -0,0 +1,233 @@
|
||||
# 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
|
||||
npx playwright install chromium
|
||||
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.
|
||||
@@ -0,0 +1,110 @@
|
||||
[package]
|
||||
name = "sqlpage"
|
||||
version = "0.44.1"
|
||||
edition = "2024"
|
||||
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-page.com/"
|
||||
repository = "https://github.com/sqlpage/SQLPage"
|
||||
documentation = "https://docs.rs/sqlpage"
|
||||
include = ["/src", "/README.md", "/build.rs", "/sqlpage"]
|
||||
|
||||
[profile.superoptimized]
|
||||
inherits = "release"
|
||||
strip = "debuginfo"
|
||||
lto = "fat"
|
||||
panic = "abort"
|
||||
codegen-units = 2
|
||||
|
||||
[dependencies]
|
||||
sqlx = { package = "sqlx-oldapi", version = "0.6.56", default-features = false, features = [
|
||||
"any",
|
||||
"runtime-tokio-rustls",
|
||||
"migrate",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mssql",
|
||||
"odbc",
|
||||
"chrono",
|
||||
"bigdecimal",
|
||||
"json",
|
||||
"uuid",
|
||||
] }
|
||||
chrono = "0.4.23"
|
||||
actix-web = { version = "4", features = ["rustls-0_23", "cookies"] }
|
||||
percent-encoding = "2.2.0"
|
||||
handlebars = "6.2.0"
|
||||
log = "0.4.17"
|
||||
mime_guess = "2.0.4"
|
||||
futures-util = "0.3.21"
|
||||
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",
|
||||
"arbitrary_precision",
|
||||
] }
|
||||
lambda-web = { version = "0.2.1", features = ["actix4"], optional = true }
|
||||
sqlparser = { version = "0.62.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.15.4", features = ["json"] }
|
||||
markdown = { version = "1", features = ["log"] }
|
||||
argon2 = "0.5.3"
|
||||
actix-web-httpauth = "0.8.0"
|
||||
rand = "0.10.0"
|
||||
actix-multipart = "0.8.0"
|
||||
base64 = "0.22"
|
||||
hmac = "0.13"
|
||||
sha2 = "0.11"
|
||||
rustls-acme = "0.15"
|
||||
dotenvy = "0.15.7"
|
||||
csv-async = { version = "1.2.6", features = ["tokio"] }
|
||||
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", optional = true }
|
||||
regex = "1"
|
||||
|
||||
# OpenTelemetry / tracing
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
||||
tracing-opentelemetry = { version = "0.33", default-features = false }
|
||||
tracing-actix-web = { version = "0.7", default-features = false }
|
||||
tracing-log = "0.2"
|
||||
opentelemetry = { version = "0.32", default-features = false, features = ["trace", "metrics"] }
|
||||
opentelemetry_sdk = { version = "0.32", default-features = false, features = ["metrics", "rt-tokio", "spec_unstable_metrics_views", "experimental_trace_batch_span_processor_with_async_runtime", "experimental_metrics_periodicreader_with_async_runtime"] }
|
||||
opentelemetry-otlp = { version = "0.32", default-features = false, features = ["http-proto", "metrics"] }
|
||||
opentelemetry-http = { version = "0.32", default-features = false }
|
||||
opentelemetry-semantic-conventions = { version = "0.32", features = ["semconv_experimental"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"]
|
||||
lambda-web = ["dep:lambda-web", "odbc-static"]
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = "3"
|
||||
tokio = { version = "1", features = ["rt", "time", "test-util"] }
|
||||
|
||||
[build-dependencies]
|
||||
awc = { version = "3", features = ["rustls-0_23-webpki-roots"] }
|
||||
rustls = "0.23"
|
||||
actix-rt = "2.8"
|
||||
libflate = "2"
|
||||
futures-util = "0.3.21"
|
||||
@@ -0,0 +1,71 @@
|
||||
FROM --platform=$BUILDPLATFORM rust:1.95-slim AS builder
|
||||
|
||||
WORKDIR /usr/src/sqlpage
|
||||
ARG TARGETARCH
|
||||
ARG BUILDARCH
|
||||
ARG CARGO_PROFILE=superoptimized
|
||||
ENV CARGO_PROFILE=$CARGO_PROFILE
|
||||
|
||||
COPY scripts/ /usr/local/bin/
|
||||
RUN cargo init .
|
||||
|
||||
RUN /usr/local/bin/setup-cross-compilation.sh "$TARGETARCH" "$BUILDARCH"
|
||||
|
||||
COPY .cargo/ .cargo/
|
||||
COPY Cargo.toml Cargo.lock build.rs ./
|
||||
COPY sqlpage/ sqlpage/
|
||||
RUN /usr/local/bin/build-dependencies.sh
|
||||
|
||||
COPY . .
|
||||
RUN /usr/local/bin/build-project.sh
|
||||
|
||||
# Default minimal image (busybox-based)
|
||||
FROM busybox:glibc AS minimal
|
||||
RUN addgroup --gid 1000 --system sqlpage && \
|
||||
adduser --uid 1000 --system --no-create-home --ingroup sqlpage sqlpage && \
|
||||
mkdir -p /etc/sqlpage && \
|
||||
touch /etc/sqlpage/sqlpage.db && \
|
||||
chown -R sqlpage:sqlpage /etc/sqlpage/sqlpage.db
|
||||
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
|
||||
# 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
|
||||
CMD ["/usr/local/bin/sqlpage"]
|
||||
|
||||
# DuckDB ODBC image (debian-based with DuckDB ODBC driver)
|
||||
FROM debian:trixie-slim AS duckdb
|
||||
|
||||
ARG TARGETARCH
|
||||
ENV SQLPAGE_WEB_ROOT=/var/www
|
||||
ENV SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage
|
||||
ENV DATABASE_URL="Driver=/opt/duckdb_odbc/libduckdb_odbc.so;Database=/var/lib/sqlpage/duckdb.db"
|
||||
|
||||
COPY scripts/install-duckdb-odbc.sh scripts/setup-sqlpage-user.sh /usr/local/bin/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
unzip \
|
||||
adduser \
|
||||
odbcinst \
|
||||
unixodbc \
|
||||
&& /usr/local/bin/install-duckdb-odbc.sh "$TARGETARCH" \
|
||||
&& apt-get purge -y --auto-remove curl unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN /usr/local/bin/setup-sqlpage-user.sh
|
||||
|
||||
COPY --from=builder /usr/src/sqlpage/sqlpage.bin /usr/local/bin/sqlpage
|
||||
|
||||
USER sqlpage
|
||||
WORKDIR /var/www
|
||||
EXPOSE 8080
|
||||
CMD ["/usr/local/bin/sqlpage"]
|
||||
|
||||
# Default stage
|
||||
FROM minimal
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Ophir LOJKINE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,369 @@
|
||||
<h1 align="center">
|
||||
SQLpage
|
||||
</h1>
|
||||
|
||||
[](./docs/sqlpage.mp4)
|
||||
|
||||
[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
|
||||
displaying your data as text, lists, grids, plots, and forms.
|
||||
|
||||
## Examples
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><td>Code<td>Result</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'list' as component,
|
||||
'Popular websites' as title;
|
||||
SELECT
|
||||
name as title,
|
||||
url as link,
|
||||
CASE type
|
||||
WHEN 1 THEN 'blue'
|
||||
ELSE 'red'
|
||||
END as color,
|
||||
description, icon, active
|
||||
FROM website;
|
||||
```
|
||||
|
||||
<td>
|
||||
|
||||

|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'chart' as component,
|
||||
'Quarterly Revenue' as title,
|
||||
'area' as type;
|
||||
|
||||
SELECT
|
||||
quarter AS x,
|
||||
SUM(revenue) AS y
|
||||
FROM finances
|
||||
GROUP BY quarter
|
||||
```
|
||||
|
||||
<td>
|
||||
|
||||

|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'form' as component,
|
||||
'User' as title,
|
||||
'Create new user' as validate;
|
||||
|
||||
SELECT
|
||||
name, type, placeholder,
|
||||
required, description
|
||||
FROM user_form;
|
||||
|
||||
INSERT INTO user
|
||||
SELECT $first_name, $last_name, $birth_date
|
||||
WHERE $first_name IS NOT NULL;
|
||||
```
|
||||
|
||||
<td>
|
||||
|
||||

|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```sql
|
||||
select 'tab' as component, true as center;
|
||||
select 'Show all cards' as title, '?' as link,
|
||||
$tab is null as active;
|
||||
select
|
||||
format('Show %s cards', color) as title,
|
||||
format('?tab=%s', color) as link,
|
||||
$tab=color as active
|
||||
from tab_example_cards
|
||||
group by color;
|
||||
|
||||
|
||||
select 'card' as component;
|
||||
select
|
||||
title, description, color
|
||||
image_url as top_image, link
|
||||
from tab_example_cards
|
||||
where $tab is null or $tab = color;
|
||||
|
||||
select
|
||||
'text' as component,
|
||||
sqlpage.read_file_as_text('footer.md') as contents_md
|
||||
```
|
||||
|
||||
<td>
|
||||
|
||||

|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Supported databases
|
||||
|
||||
- [SQLite](https://www.sqlite.org/index.html), including the ability to [load extensions](./configuration.md) such as *Spatialite*.
|
||||
- [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-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/sqlpage/SQLPage/releases).
|
||||
|
||||
- Download the binary that corresponds to your operating system (linux, macos, or windows).
|
||||
- Uncompress it: `tar -xzf sqlpage-*.tgz`
|
||||
- Run it: `./sqlpage.bin`
|
||||
|
||||
### With docker
|
||||
|
||||
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:
|
||||
- `docker run -it --name sqlpage -p 8080:8080 --volume "$(pwd):/var/www" --rm lovasoa/sqlpage`
|
||||
- (`"$(pwd):/var/www"` allows sqlpage to run sql files from your current working directory)
|
||||
- Create a file called index.sql with the contents from [this example](./index.sql)
|
||||
- Open https://localhost:8080 in your browser
|
||||
- Optionally, you can also mount a directory containing sqlpage's configuration file,
|
||||
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 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
|
||||
FROM debian:stable-slim
|
||||
COPY --from=lovasoa/sqlpage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage
|
||||
```
|
||||
|
||||
We provide compiled binaries only for the x86_64 architecture, but provide docker images for other architectures, including arm64 and armv7. If you want to run SQLPage on a Raspberry Pi or
|
||||
a cheaper ARM cloud instance, using the docker image is the easiest way to do it.
|
||||
|
||||
### On Mac OS, with homebrew
|
||||
|
||||
An alternative for Mac OS users is to use [SQLPage's homebrew package](https://formulae.brew.sh/formula/sqlpage).
|
||||
|
||||
- [Install homebrew](https://brew.sh/)
|
||||
- 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)
|
||||
- If you use docker, a DuckDB-enabled image variant is available with pre-installed DuckDB ODBC drivers
|
||||
- Use the `-duckdb` suffix: `lovasoa/sqlpage:main-duckdb` or `lovasoa/sqlpage:latest-duckdb`
|
||||
- Comes pre-configured to connect to DuckDB at `/var/lib/sqlpage/duckdb.db` inside the container
|
||||
- To customize [connection options](https://duckdb.org/docs/stable/clients/odbc/configuration), set `DATABASE_URL`:
|
||||
- `docker run -e DATABASE_URL="Driver=DuckDB;Database=/path/to/your.db" -p 8080:8080 lovasoa/sqlpage:main-duckdb`
|
||||
- To persist your DuckDB database, mount a volume:
|
||||
- `docker run -v ./data:/var/lib/sqlpage lovasoa/sqlpage:main-duckdb`
|
||||
- [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
|
||||
|
||||

|
||||
|
||||
SQLPage is a [web server](https://en.wikipedia.org/wiki/Web_server) written in
|
||||
[rust](https://en.wikipedia.org/wiki/Rust_(programming_language))
|
||||
and distributed as a single executable file.
|
||||
When it receives a request to 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.
|
||||
When the database starts returning rows for the query,
|
||||
SQLPage maps each piece of information in the row to a parameter
|
||||
in one of its pre-defined components' templates, and streams the result back
|
||||
to the user's browser.
|
||||
|
||||
## Examples
|
||||
|
||||
- [TODO list](./examples/todo%20application/): a simple todo list application, illustrating how to create a basic CRUD application with SQLPage.
|
||||
- [Plots, Tables, forms, and interactivity](./examples/plots%20tables%20and%20forms/): a short well-commented demo showing how to use plots, tables, forms, and interactivity to filter data based on an URL parameter.
|
||||
- [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-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.
|
||||
- [Handling file uploads](./examples/image%20gallery%20with%20user%20uploads): An image gallery where authenticated users can publish new images via an upload form.
|
||||
- [Bulk data import from CSV files](./examples/official-site/examples/handle_csv_upload.sql) : A simple form letting users import CSV files to fill a database table.
|
||||
- [Advanced authentication example using PostgreSQL stored procedures](https://github.com/mnesarco/sqlpage_auth_example)
|
||||
- [Complex web application in SQLite with user management, file uploads, plots, maps, tables, menus, ...](https://github.com/DSMejantel/Ecole_inclusive)
|
||||
- [Single sign-on](./examples/single%20sign%20on): An example of how to implement OAuth and OpenID Connect (OIDC) authentication in SQLPage. The demo also includes a CAS (Central Authentication Service) client.
|
||||
- [Dark theme](./examples/light-dark-toggle/) : demonstrates how to let the user toggle between a light theme and a dark theme, and store the user's preference.
|
||||
|
||||
You can try all the examples online without installing anything on your computer using [SQLPage's online demo on replit](https://replit.com/@pimaj62145/SQLPage).
|
||||
|
||||
## Configuration
|
||||
|
||||
SQLPage can be configured through either a configuration file placed in `sqlpage/sqlpage.json`
|
||||
or environment variables such as `DATABASE_URL` or `LISTEN_ON`.
|
||||
|
||||
For more information, read [`configuration.md`](./configuration.md).
|
||||
|
||||
Additionally, custom components can be created by placing [`.handlebars`](https://handlebarsjs.com/guide/)
|
||||
files in `sqlpage/templates`. [Example](./sqlpage/templates/card.handlebars).
|
||||
|
||||
### HTTPS
|
||||
|
||||
SQLPage supports HTTP/2 and HTTPS natively and transparently.
|
||||
Just set `SQLPAGE_HTTPS_DOMAIN=example.com`, and SQLPage
|
||||
will automatically request a trusted certificate and
|
||||
start encrypting all your user's traffic with it.
|
||||
No tedious manual configuration for you,
|
||||
and no annoying "Connection is Not Secure" messages for your users !
|
||||
|
||||
## Serverless
|
||||
|
||||
You can run SQLpage [serverless](https://en.wikipedia.org/wiki/Serverless_computing)
|
||||
by compiling it to an [AWS Lambda function](https://aws.amazon.com/lambda/).
|
||||
An easy way to do so is using the provided docker image:
|
||||
|
||||
```bash
|
||||
docker build -t sqlpage-lambda-builder . -f lambda.Dockerfile --target builder
|
||||
docker run sqlpage-lambda-builder cat deploy.zip > sqlpage-aws-lambda.zip
|
||||
```
|
||||
|
||||
You can then just add your own SQL files to `sqlpage-aws-lambda.zip`,
|
||||
and [upload it to AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-package.html#gettingstarted-package-zip),
|
||||
selecting *Custom runtime on Amazon Linux 2* as a runtime.
|
||||
|
||||
### Hosting sql files directly inside the database
|
||||
|
||||
When running serverless, you can include the SQL files directly in the image that you are deploying.
|
||||
But if you want to be able to update your sql files on the fly without creating a new image,
|
||||
you can store the files directly inside the database, in a table that has the following structure:
|
||||
|
||||
```sql
|
||||
CREATE TABLE sqlpage_files(
|
||||
path VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
contents BLOB,
|
||||
last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
Make sure to update `last_modified` every time you update the contents of a file (or do it inside a TRIGGER).
|
||||
SQLPage will re-parse a file from the database only when it has been modified.
|
||||
|
||||
## Technologies and libraries used
|
||||
|
||||
- [actix web](https://actix.rs/) handles HTTP requests at an incredible speed,
|
||||
- [tabler](https://preview.tabler.io) handles the styling for professional-looking clean components,
|
||||
- [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
|
||||
|
||||
> **Why use SQL instead of a real programming language? SQL isn't even [Turing-complete](https://en.wikipedia.org/wiki/Turing_completeness)!**
|
||||
|
||||
- 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)
|
||||
|
||||
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?**
|
||||
|
||||
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?**
|
||||
|
||||
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.**
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
[](https://github.com/sqlpage/SQLPage/releases/latest)
|
||||
[](https://hub.docker.com/r/sqlpage/SQLPage)
|
||||
[](https://formulae.brew.sh/formula/sqlpage#default)
|
||||
[](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116)
|
||||
[](https://crates.io/crates/sqlpage)
|
||||
[)](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
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`sqlpage/SQLPage`
|
||||
- 原始仓库:https://github.com/sqlpage/SQLPage
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,190 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report suspected SQLPage vulnerabilities privately to
|
||||
`contact@ophir.dev`.
|
||||
|
||||
Include the SQLPage version or commit, database backend, relevant
|
||||
configuration, a minimal SQL file if possible, and the exact attacker-controlled
|
||||
input. Do not open a public issue for a non-public vulnerability.
|
||||
|
||||
## Threat Model
|
||||
|
||||
SQLPage is a runtime for applications written in SQL. It maps HTTP requests to
|
||||
SQL files, executes those files on the configured database, and renders the
|
||||
result. SQLPage is not a sandbox for SQLPage application authors or operators.
|
||||
|
||||
### Trusted Inputs
|
||||
|
||||
SQLPage trusts the application and deployment:
|
||||
|
||||
- application files under `web_root`;
|
||||
- application files stored in the optional `sqlpage_files` table;
|
||||
- configuration, command-line arguments, environment variables, and `.env`
|
||||
files;
|
||||
- templates, migrations, and connection-management SQL in the configuration
|
||||
directory;
|
||||
- database behavior and access: roles, permissions, schema, extensions,
|
||||
triggers, stored procedures, views, and migrations;
|
||||
- anyone or anything that can modify one of the above.
|
||||
|
||||
Control of trusted inputs is control of the SQLPage application. If an attacker
|
||||
can edit `sqlpage.json`, alter templates, change OIDC path rules, enable
|
||||
dangerous Markdown options, enable `allow_exec`, or write SQL into
|
||||
`sqlpage_files`, that is outside SQLPage's vulnerability boundary unless
|
||||
SQLPage itself granted that access to an untrusted actor.
|
||||
|
||||
### Untrusted Inputs
|
||||
|
||||
SQLPage does not trust remote inputs:
|
||||
|
||||
- HTTP paths, query strings, form fields, request bodies, and multipart uploads;
|
||||
- uploaded filenames, MIME types, and file contents;
|
||||
- HTTP headers, cookies, Basic Auth credentials, and unauthenticated OIDC
|
||||
callback parameters;
|
||||
- responses from remote servers contacted with `sqlpage.fetch` or
|
||||
`sqlpage.fetch_with_meta`.
|
||||
|
||||
Database row values are not classified globally as trusted or untrusted.
|
||||
SQLPage cannot know whether a row came from an administrator-maintained table,
|
||||
user-generated content, a trigger, or a third-party import. The security
|
||||
boundary depends on where trusted SQL places the value.
|
||||
|
||||
### Data Positions and Instruction Positions
|
||||
|
||||
Trusted SQL chooses whether a value is ordinary data or an instruction to
|
||||
SQLPage.
|
||||
|
||||
Data positions are values SQLPage should render, serialize, encode, or pass as
|
||||
bound database parameters without giving them extra authority. Examples include
|
||||
ordinary table cells, text fields, JSON response data, CSV cells, and safe
|
||||
Markdown.
|
||||
|
||||
Instruction positions are values SQLPage intentionally treats as application
|
||||
instructions or capability arguments. Examples include:
|
||||
|
||||
- component names;
|
||||
- `dynamic` component properties;
|
||||
- response status codes, headers, redirects, cookies, and downloads;
|
||||
- file paths passed to `run_sql`, `read_file_as_text`, or
|
||||
`read_file_as_data_url`;
|
||||
- URLs and request options passed to `fetch` or `fetch_with_meta`;
|
||||
- raw HTML and unsafe Markdown;
|
||||
- command names and arguments passed to `exec` when `allow_exec` is enabled.
|
||||
|
||||
Placing a database value in an instruction position is an application decision.
|
||||
For example, `select sqlpage.read_file_as_text(f) from trusted_table` is allowed
|
||||
by design. If someone who can modify `trusted_table` can read arbitrary files,
|
||||
that is a problem in the application or database permissions, not SQLPage.
|
||||
|
||||
SQLPage's responsibility is to enforce the documented meaning and guardrails of
|
||||
each position: data positions must not become instructions, instruction
|
||||
positions must not bypass their documented checks, and malformed values must not
|
||||
crash SQLPage or escape into unrelated capabilities.
|
||||
|
||||
## In Scope
|
||||
|
||||
Please report cases where SQLPage itself crosses that boundary. Examples:
|
||||
|
||||
- An HTTP request can execute attacker-chosen SQL without trusted SQL explicitly
|
||||
exposing that behavior.
|
||||
- SQLPage parameter handling turns `$name`, `:name`, or `?name` into executable
|
||||
SQL instead of a bound database value.
|
||||
- A value in a data position causes SQL execution, command execution, host file
|
||||
access, response-header injection, unsafe HTML execution, or another
|
||||
instruction-position effect.
|
||||
- A database value in any position reliably crashes or panics SQLPage instead
|
||||
of producing a response or application-level error.
|
||||
- HTTP routing, path decoding, static file serving, caching, `run_sql`, or file
|
||||
functions expose host files that trusted SQL or configuration did not select.
|
||||
- Reserved private files, including the `sqlpage/` prefix, dotfiles,
|
||||
templates, and configuration, are reachable over HTTP.
|
||||
- `allow_exec` is false, but an attacker can execute a local command through
|
||||
SQLPage.
|
||||
- Built-in OIDC handling accepts forged, expired, wrong-issuer,
|
||||
wrong-audience, wrong-nonce, or wrong-signature tokens, or applies configured
|
||||
public/protected path rules incorrectly.
|
||||
- Default-safe rendering or safe Markdown executes browser script.
|
||||
- SQLPage-generated production error responses expose source code, stack
|
||||
traces, SQL text, parameters, environment values, or configuration values.
|
||||
- Upload handling allows path traversal, overwrite of unintended files, or file
|
||||
disclosure without trusted SQL selecting that behavior.
|
||||
- Official SQLPage documentation or examples recommend placing untrusted remote
|
||||
input into an instruction position without validation.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are usually application or deployment vulnerabilities, not
|
||||
SQLPage vulnerabilities:
|
||||
|
||||
- Trusted SQL omits authentication or authorization checks.
|
||||
- Trusted SQL, a stored procedure, trigger, view, or extension builds and
|
||||
executes SQL from untrusted data.
|
||||
- Trusted SQL selects a value into an instruction position such as `component`,
|
||||
`dynamic.properties`, a redirect target, header value, file path, `run_sql`
|
||||
target, `fetch` URL, raw HTML, unsafe Markdown, or `exec` argument.
|
||||
- Trusted SQL intentionally reads a host file, including an absolute path, and
|
||||
returns it to a client.
|
||||
- An operator intentionally changes configuration to expose files, trust a
|
||||
different database, make an OIDC path public, weaken CSP, enable dangerous
|
||||
Markdown options, load SQLite extensions, or enable `allow_exec`.
|
||||
- A symlink placed under `web_root` exposes its target. SQLPage follows
|
||||
symlinks during static file serving, so operators must not create symlinks
|
||||
under `web_root` that point to reserved or private files, such as the
|
||||
`sqlpage/` configuration directory or dotfiles, or to files outside
|
||||
`web_root`, since those targets would then be publicly reachable over HTTP.
|
||||
- An attacker can modify SQL files, templates, configuration, environment
|
||||
variables, migrations, database code, or `sqlpage_files`.
|
||||
- The configured database role has broader permissions than the application
|
||||
needs.
|
||||
- A SQLPage application is publicly reachable because no authentication was
|
||||
configured.
|
||||
- An attacker can plant or overwrite cookies for the SQLPage origin (for
|
||||
example through a compromised subdomain, a sibling application on a shared
|
||||
parent domain, or a man-in-the-middle on plain HTTP). Attacks that depend on
|
||||
injecting attacker-chosen cookies into the victim's browser, such as OIDC
|
||||
login CSRF or session fixation via a forged login-flow-state cookie, are out
|
||||
of scope. SQLPage assumes its origin's cookie jar is writable only by the
|
||||
user agent, not by attackers.
|
||||
- Trusted SQL asks SQLPage or the database to perform expensive work.
|
||||
|
||||
These may still be serious and should be fixed in the affected application,
|
||||
deployment, or documentation.
|
||||
|
||||
## Boundary Examples
|
||||
|
||||
Report: `/x.sql?sort=...` causes SQLPage to execute attacker-chosen SQL because
|
||||
SQLPage rewrote a parameter incorrectly.
|
||||
|
||||
Do not report as SQLPage: `x.sql` passes `sort` to a stored procedure that
|
||||
concatenates it into dynamic SQL.
|
||||
|
||||
Report: a normal table cell containing `<script>` executes script in the
|
||||
browser when rendered by a default-safe component.
|
||||
|
||||
Do not report as SQLPage: trusted SQL selects that value as `html`,
|
||||
`unsafe_contents_md`, `component`, or `dynamic.properties`.
|
||||
|
||||
Report: a specific string returned by the database in a text column panics the
|
||||
SQLPage process.
|
||||
|
||||
Do not report as SQLPage: trusted SQL passes that string as the filename
|
||||
argument to `sqlpage.read_file_as_text`.
|
||||
|
||||
Report: `/..%2F..%2Fetc%2Fpasswd` or `/sqlpage/sqlpage.json` is served directly
|
||||
by SQLPage.
|
||||
|
||||
Do not report as SQLPage: trusted SQL calls
|
||||
`sqlpage.read_file_as_text('/etc/passwd')` and renders the result.
|
||||
|
||||
Report: an unauthenticated request can write a new SQL file into
|
||||
`sqlpage_files` through an unintended SQLPage endpoint.
|
||||
|
||||
Do not report as SQLPage: an administrator, migration, or intentionally exposed
|
||||
application page writes SQL into `sqlpage_files`.
|
||||
|
||||
Report: official documentation recommends `sqlpage.run_sql($user_input)`.
|
||||
|
||||
Do not report as SQLPage: a private application uses
|
||||
`sqlpage.run_sql($user_input)` despite the documentation warning against it.
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!examples/official-site/pgconf/**",
|
||||
"!tests/end-to-end/test-results/**",
|
||||
"!.zed/*.json"
|
||||
],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"useIgnoreFile": true,
|
||||
"clientKind": "git"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
use actix_rt::spawn;
|
||||
use actix_rt::time::sleep;
|
||||
use libflate::gzip;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs::File;
|
||||
use std::hash::Hasher;
|
||||
use std::io::Read;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
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_tabler_icons(
|
||||
c.clone(),
|
||||
"https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.44.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 {
|
||||
awc::ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.no_default_headers()
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Creates a file with inlined remote files included
|
||||
async fn download_deps(client: Rc<awc::Client>, filename: &str) {
|
||||
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}");
|
||||
let original = File::open(path_in).unwrap();
|
||||
process_input_file(&client, &path_out, original).await;
|
||||
std::fs::write(
|
||||
format!("{}.filename.txt", path_out.display()),
|
||||
hashed_filename(&path_out),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn process_input_file(client: &awc::Client, path_out: &Path, original: File) {
|
||||
let mut outfile = gzip::Encoder::new(File::create(path_out).unwrap()).unwrap();
|
||||
for l in BufReader::new(original).lines() {
|
||||
let line = l.unwrap();
|
||||
if line.starts_with("/* !include https://") {
|
||||
let url = line
|
||||
.trim_start_matches("/* !include ")
|
||||
.trim_end_matches(" */");
|
||||
if std::env::var("DOCS_RS").is_err() {
|
||||
copy_url_to_opened_file(client, url, &mut outfile).await;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
outfile.write_all(b"\n").unwrap();
|
||||
} else {
|
||||
writeln!(outfile, "{line}").unwrap();
|
||||
}
|
||||
}
|
||||
outfile
|
||||
.finish()
|
||||
.as_result()
|
||||
.expect("Unable to write compressed frontend asset");
|
||||
}
|
||||
|
||||
async fn copy_url_to_opened_file(
|
||||
client: &awc::Client,
|
||||
url: &str,
|
||||
outfile: &mut impl std::io::Write,
|
||||
) {
|
||||
// If the file has been downloaded manually, use it
|
||||
let cached_file_path = make_url_path(url);
|
||||
if !cached_file_path.exists() {
|
||||
download_url_to_path(client, url, &cached_file_path).await;
|
||||
println!("cargo:rerun-if-changed={}", cached_file_path.display());
|
||||
}
|
||||
copy_cached_to_opened_file(&cached_file_path, outfile);
|
||||
}
|
||||
|
||||
fn copy_cached_to_opened_file(source: &Path, outfile: &mut impl std::io::Write) {
|
||||
let reader = std::fs::File::open(source).unwrap();
|
||||
let mut buf = std::io::BufReader::new(reader);
|
||||
// Not async, but performance should not really matter here
|
||||
std::io::copy(&mut buf, outfile).unwrap();
|
||||
}
|
||||
|
||||
async fn download_url_to_path(client: &awc::Client, url: &str, path: &Path) {
|
||||
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;
|
||||
eprintln!("Retrying download of {url} after {err}.");
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given a filename, creates a new unique filename based on the file contents
|
||||
fn hashed_filename(path: &Path) -> String {
|
||||
let mut file = File::open(path).unwrap();
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut hasher = DefaultHasher::new();
|
||||
loop {
|
||||
let bytes_read = file
|
||||
.read(&mut buf)
|
||||
.unwrap_or_else(|e| panic!("error reading '{}': {}", path.display(), e));
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.write(&buf[..bytes_read]);
|
||||
}
|
||||
let hash = hasher.finish();
|
||||
format!(
|
||||
"{}.{:x}.{}",
|
||||
path.file_stem().unwrap().to_str().unwrap(),
|
||||
hash,
|
||||
path.extension().unwrap().to_str().unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
fn make_url_path(url: &str) -> PathBuf {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let sqlpage_artefacts = Path::new(&manifest_dir)
|
||||
.join("target")
|
||||
.join("sqlpage_artefacts");
|
||||
std::fs::create_dir_all(&sqlpage_artefacts).unwrap();
|
||||
let filename = url.replace(
|
||||
|c: char| !c.is_ascii_alphanumeric() && !['.', '-'].contains(&c),
|
||||
"_",
|
||||
);
|
||||
sqlpage_artefacts.join(filename)
|
||||
}
|
||||
|
||||
async fn download_tabler_icons(client: Rc<awc::Client>, 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, "<symbol", "</symbol>") {
|
||||
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/<target>-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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
# Configuring SQLPage
|
||||
|
||||
SQLPage can be configured through either [environment variables](https://en.wikipedia.org/wiki/Environment_variable)
|
||||
or a [JSON](https://en.wikipedia.org/wiki/JSON) file placed in `sqlpage/sqlpage.json`.
|
||||
|
||||
You can find an example configuration file in [`sqlpage/sqlpage.json`](./sqlpage/sqlpage.json).
|
||||
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` or `DSN=DuckDB` | Database connection URL, either `dbengine://user:password@host:port/dbname` or an ODBC connection string. Special characters should be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). See [Database connection strings](#database-connection-strings) for details and examples.|
|
||||
| `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<BR> MySql: 75<BR> SQLite: 16<BR> MSSQL: 100 | How many simultaneous database connections to open at most |
|
||||
| `database_connection_idle_timeout_seconds` | SQLite: None<BR> All other: 30 minutes | Automatically close database connections after this period of inactivity. Set to 0 to disable. |
|
||||
| `database_connection_max_lifetime_seconds` | SQLite: None<BR> All other: 60 minutes | Always close database connections after this amount of time. Set to 0 to disable. |
|
||||
| `database_connection_retries` | 6 | Database connection attempts before giving up. Retries will happen every 5 seconds. |
|
||||
| `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. |
|
||||
| `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. Static file serving follows symlinks, so do not place symlinks under `web_root` that point to private paths (such as the `sqlpage/` config directory) or to files outside `web_root`, as their targets would become publicly reachable (see [`SECURITY.md`](./SECURITY.md)). |
|
||||
| `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-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 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"]`. All paths must start with a "/" and will be prepended by `site_prefix` if defined.|
|
||||
| `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. All paths must start with a "/" and will be prepended by `site_prefix` if defined.
|
||||
| `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` | 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@<https_domain> | 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. |
|
||||
| `cache_stale_duration_ms` | 1000 (prod), 0 (dev) | The duration in milliseconds that a file can be cached before its freshness is checked against the filesystem. Defaults to 1000ms (1 second) in production and 0ms in development. |
|
||||
| `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.
|
||||
|
||||
## Environment variables
|
||||
|
||||
All the parameters above can be set through environment variables.
|
||||
|
||||
The name of the environment variable is the same as the name of the configuration variable,
|
||||
but in uppercase.
|
||||
|
||||
The environment variable name can optionally be prefixed with `SQLPAGE_`.
|
||||
|
||||
Additionnally, when troubleshooting, you can set the
|
||||
[`LOG_LEVEL`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
|
||||
environment variable to `sqlpage=debug` to get more detailed logs and see exactly what SQLPage is doing.
|
||||
Request-completion access logs use the target `sqlpage::access`. Broad filters such as
|
||||
`sqlpage=info` include them, but target-specific filters such as `sqlpage::webserver::http=info`
|
||||
must also include `sqlpage::access=info` if you want to keep request logs.
|
||||
|
||||
SQLPage also supports [OpenTelemetry](https://opentelemetry.io/) tracing via the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. See the [SQLPage monitoring example](https://github.com/sqlpage/sqlpage/tree/main/examples/telemetry).
|
||||
|
||||
If you have a `.env` file in the current directory or in any of its parent directories, SQLPage will automatically load environment variables from it.
|
||||
|
||||
### Database connection strings
|
||||
|
||||
The `database_url` parameter sets all the connection parameters for the database, including
|
||||
|
||||
- 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
|
||||
- any additional parameters, including
|
||||
- `mode=rwc` for SQLite to allow read-write connections
|
||||
- `sslmode=require` (or `disable`, `allow`, `verify-ca`, `verify-full`)
|
||||
for PostgreSQL to enable or disable SSL
|
||||
- `sslrootcert=/path/to/ca.pem` for PostgreSQL to specify the path to the CA certificate file
|
||||
- `sslcert=/path/to/cert.pem` to specify the path to the TLS client certificate file and `sslkey=/path/to/key.pem` to specify the path to the TLS client key file for PostgreSQL and MySQL.
|
||||
- `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 like `@` (`%40`), `:` (`%3A`), `/` (`%2F`), `?` (`%3F`), `#` (`%23`).
|
||||
|
||||
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="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"
|
||||
```
|
||||
|
||||
## Custom components
|
||||
|
||||
SQLPage allows you to create custom components in addition to or instead of the default ones.
|
||||
To create a custom component, create a [`.handlebars`](https://handlebarsjs.com/guide/expressions.html)
|
||||
file in the `sqlpage/templates` directory of your SQLPage installation.
|
||||
|
||||
For instance, if you want to create a custom `my_component` component, that displays the value of the `my_column` column, create a `sqlpage/templates/my_component.handlebars` file with the following content:
|
||||
|
||||
```handlebars
|
||||
<ul>
|
||||
{{#each_row}}
|
||||
<li>Value of my column: {{my_column}}</li>
|
||||
{{/each_row}}
|
||||
</ul>
|
||||
```
|
||||
|
||||
[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.
|
||||
|
||||
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.
|
||||
|
||||
This can be useful to set up the database connection for your application.
|
||||
For instance, on postgres, you can use this to [set the `search path` and various other connection options](https://www.postgresql.org/docs/current/sql-set.html).
|
||||
|
||||
```sql
|
||||
SET TIME ZONE 'UTC';
|
||||
SET search_path = my_schema;
|
||||
```
|
||||
|
||||
On SQLite, you can use this to [`ATTACH`](https://www.sqlite.org/lang_attach.html) additional databases.
|
||||
|
||||
```sql
|
||||
ATTACH DATABASE '/path/to/my_other_database.db' AS my_other_database;
|
||||
```
|
||||
|
||||
(and then, you can use `my_other_database.my_table` in your queries)
|
||||
|
||||
You can also use this to create *temporary tables* to store intermediate results that are useful in your SQLPage application, but that you don't want to store permanently in the database.
|
||||
|
||||
```sql
|
||||
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).
|
||||
@@ -0,0 +1,24 @@
|
||||
ARG VERSION=2022-latest
|
||||
FROM mcr.microsoft.com/mssql/server:${VERSION}
|
||||
|
||||
# Create a config directory
|
||||
USER root
|
||||
RUN mkdir -p /usr/config
|
||||
WORKDIR /usr/config
|
||||
|
||||
# Bundle config source
|
||||
COPY entrypoint.sh /usr/config/entrypoint.sh
|
||||
COPY setup.sql /usr/config/setup.sql
|
||||
|
||||
# Grant permissions for to our scripts to be executable
|
||||
RUN chmod +x /usr/config/entrypoint.sh
|
||||
RUN chown 10001 /usr/config/entrypoint.sh
|
||||
USER 10001
|
||||
|
||||
ENV SA_PASSWORD="Password123!"
|
||||
ENV ACCEPT_EULA="Y"
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=10 \
|
||||
CMD sqlcmd -S localhost -U root -P "Password123!" -Q "SELECT 1" || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/config/entrypoint.sh"]
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
/opt/mssql/bin/sqlservr &
|
||||
pid=$!
|
||||
|
||||
# Wait 15 seconds for SQL Server to start up
|
||||
sleep 15
|
||||
|
||||
# Run the setup script to create the DB and the schema in the DB
|
||||
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql -No
|
||||
|
||||
# Wait for sqlservr to exit
|
||||
wait -n $pid
|
||||
@@ -0,0 +1,16 @@
|
||||
IF DB_ID('sqlpage') IS NULL
|
||||
BEGIN
|
||||
CREATE DATABASE sqlpage;
|
||||
END;
|
||||
GO
|
||||
|
||||
USE sqlpage;
|
||||
GO
|
||||
|
||||
CREATE LOGIN root WITH PASSWORD = 'Password123!';
|
||||
CREATE USER root FOR LOGIN root;
|
||||
GO
|
||||
|
||||
GRANT CREATE TABLE TO root;
|
||||
GRANT ALTER, DELETE, INSERT, SELECT, UPDATE ON SCHEMA::dbo TO root;
|
||||
GO
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM postgres:18-alpine
|
||||
|
||||
# Copy the SSL certificates
|
||||
|
||||
COPY --chown=postgres:postgres ./ssl /ssl
|
||||
RUN chmod 700 /ssl && chmod 600 /ssl/*
|
||||
|
||||
CMD ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/ssl/server.crt", "-c", "ssl_key_file=/ssl/server.key"]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Postgres SSL setup
|
||||
|
||||
This directory contains dummy SSL certificates for testing purposes.
|
||||
Don't use these certificates for anything other than testing.
|
||||
|
||||
The goal is to test the ability of SQLPage to connect to a Postgres database using SSL.
|
||||
@@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI3Ywr4wOmP2kCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECImnmpeLNbLFBIIEyCXo6EAO1UOs
|
||||
iSLKcb8jqXd+ZkvGD3BHNxoT9aIOmv81GFwIgCbuK6qhXloMa7PT8IwFBF78JYAZ
|
||||
flAG1cAlxZjPqR+ZEtaAd0vdNHFPtvgCMO9usgP4PGfuFQbpQIJL0DoUZs8iLgdp
|
||||
v6d1dGzxABq1kx9RHz2f/cgZXvBPphMcGbQFIpc4G2l1KjEJB4SnaiwrGexa3X3d
|
||||
K48lXlU7Zlp0FbNq8cTXBtFmIInVZmuoZOySITT/vBlp9NKfh++jwaR7Ney/nNeE
|
||||
VvIhKE+S+msbpwzI9mtn0h0vjLPVd+ZK4kLGzFuTnYRoJuHEO2MBJ+bJaxEG0Kn1
|
||||
klST2R236PHnc3JBFgWP8tHtxn54btv0RuwwWVwJUaQLsdFi5VznSDPqMWJQdaOQ
|
||||
MU0SgzZEEJksYfxfdofTPtcoTKYLEA2hG3bwuyhMHd9POSbhTXfeVn3HGRvDYZuv
|
||||
iskvZ8kViPnsk6TkGwXVWsqjMUknybddMhIxVo6ob5JBCjGskUIfmPIkoWkgv6JT
|
||||
lYT/KP+uPvWccrsYtoCoqnVnYI3oDDuShUWd/VJ6gAQ62uGTwpAg4F96INvbhS6r
|
||||
eo+4PatfBggxJo5m2pIw1HiqLtthJ32u1kTlsrf64slWKsA/GoiBWDUfPydYI5Tn
|
||||
38gth53MDz2b+g2/unCd9dEyeQzDhdpgvHzpZYEmKmzrhOYRMiyryC3A4nO8LEwa
|
||||
saAhKypMI9l3aG/55fYiZT41srSCTEM9cynsdjfm4QCvGR0rsYgqLel+8/1ooCp8
|
||||
mSn+ldnx53hDSXA/kbynJelL7hmpMgZ198pnp3uRZbkDxQ7iawf6L7UPslhjo3Qh
|
||||
F7a/FvN/fYTIZ0u4/iqRVuXTI1RZNfoYaiVLZh/wxkTBSJPfMfVFtxwNHMrqF6Kw
|
||||
Do8wvPxqi/dMs1IKsChC7+VuVHn1JlZAfy5/4EaqK0twh5bXDgIT4RnmS25JAyC8
|
||||
o6y5s2Bzl6jVlssfTrrmlgjrgQrglP+rJqVf9Vv5v9Kffu1jUfNJ9/DWXgn8aQd8
|
||||
qBL+2Up/hxPE6YYqvZsQwUdjmpMDaGsUAnVgJbyp2eoC7c0giU9OKDsHdVM6XNcQ
|
||||
WzT+e29WC43OWIiL+mWS6lFWZnShH34moVbjcvhCVNLV5wnkPkNOUfuLy2zb6fkm
|
||||
sDgxjh3/zdodvvclKQpfzFmXTxBi9UBUnPs03ssL4FLgSSbRMhsV+zj6utmlYmTU
|
||||
koFZ6tR9IWZfIP6RgY5BOCinOu9rfgTlTKNRFrOmc02hINHw/QjWJB3LK7C/7UBc
|
||||
lp81AYiHumwd6eArK30IoQb/uhFGGbVzVxuad5P/W0CYX5W7TtnXjVpOEdmLKZxg
|
||||
Qgoq0AoLLZXViaoTdxunv6roJ0qo4almyf0Jyx8Vas2XYbifJlx2x04Uxvjm/GhM
|
||||
2W9EIDYZgzosbe2ON0XeeAJ09xZUxQZtmJrsxrBQ8FSpkCL8iO4kBWQlo5ktyLcv
|
||||
GN0Q78efDqlGD4rvDCKL5MJjxOpk0CQJ0mdTdA4DGldFlMmVFlTEy9P+ztpyRfU2
|
||||
2Bkq9ByTtKQH0T+z/JQTnX/jb9WsELEOJ4FkddmnzRWYSRclKl4+iNzRuk5it7th
|
||||
IDfEy0du+0vnOa0xObrUnQ==
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -0,0 +1,77 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
5a:b5:ee:1b:4c:15:cf:08:8b:63:87:39:12:61:bc:97:c2:55:19:af
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN = postgres
|
||||
Validity
|
||||
Not Before: May 2 12:54:42 2024 GMT
|
||||
Not After : Jun 1 12:54:42 2024 GMT
|
||||
Subject: CN = postgres
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:d4:f0:6b:40:ec:46:a2:0d:94:1b:ea:54:f0:80:
|
||||
3f:39:4b:16:38:9b:c2:d7:a3:dd:00:03:d8:3d:64:
|
||||
b0:65:03:6f:4a:0f:6f:95:c1:ed:0f:2a:91:33:fb:
|
||||
f7:fc:47:4c:59:b6:97:6c:09:4a:e6:ba:ce:d2:c8:
|
||||
1d:7b:81:d0:17:e4:d9:42:85:95:df:12:1d:59:c0:
|
||||
f3:b3:85:28:7c:ce:29:df:23:b7:4c:a2:77:8a:df:
|
||||
2f:ac:df:41:84:1e:06:c0:7b:f0:f4:1c:ba:9e:af:
|
||||
bd:87:6d:8b:1c:8f:49:05:5a:d6:b3:06:68:83:aa:
|
||||
38:96:27:85:30:bd:82:10:e9:f7:b1:a3:09:4f:af:
|
||||
42:3d:c8:8a:63:40:4e:a1:95:f4:2e:3a:81:95:d3:
|
||||
17:41:f0:20:9f:e9:37:2b:52:97:7f:11:4e:86:93:
|
||||
6f:57:1a:35:d3:93:83:c5:b6:57:ec:f3:f7:46:1c:
|
||||
d4:26:b8:e4:10:77:d9:d0:7f:4b:b5:a5:62:d4:9d:
|
||||
7f:3a:d6:7f:1b:9c:18:2b:2b:c2:35:3b:a6:80:c6:
|
||||
c9:52:0f:06:4d:7f:da:88:38:7e:25:bc:7c:16:ae:
|
||||
3f:fc:78:da:6b:7c:28:8b:f4:df:d9:0c:3d:64:94:
|
||||
29:82:96:e8:82:4e:90:d5:e6:a6:d6:89:c2:eb:22:
|
||||
cc:ad
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
69:AD:45:23:C0:49:B4:44:05:2D:A4:01:9C:EF:EC:0A:F4:F9:78:90
|
||||
X509v3 Authority Key Identifier:
|
||||
69:AD:45:23:C0:49:B4:44:05:2D:A4:01:9C:EF:EC:0A:F4:F9:78:90
|
||||
X509v3 Basic Constraints: critical
|
||||
CA:TRUE
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
5e:5d:4f:67:6e:3b:57:b6:5a:8d:1d:e8:dc:8c:be:29:8e:f5:
|
||||
29:43:48:34:d1:e3:a3:aa:4e:99:12:b7:3d:ed:a2:8e:3c:cd:
|
||||
ff:82:d3:d0:61:99:e3:40:1c:14:b3:87:86:43:7f:eb:22:bd:
|
||||
95:ff:64:e9:3f:ba:e5:74:6e:62:92:80:16:83:ec:f6:6e:1b:
|
||||
5c:8e:7e:65:ee:7d:bf:5a:18:6e:47:7e:86:60:70:36:38:9f:
|
||||
26:d4:cf:f1:0d:9d:23:43:72:b9:2e:43:4a:3a:4d:b0:04:1d:
|
||||
6d:e6:f4:2b:77:c3:75:eb:f2:4a:ac:3d:87:5b:73:cb:67:da:
|
||||
e8:82:6b:d0:1b:2e:55:93:e4:c9:e6:80:e7:ee:41:d0:50:87:
|
||||
83:3f:81:aa:50:71:9c:3e:81:b6:52:9a:40:4c:e1:97:29:c9:
|
||||
8b:5e:44:cc:aa:d9:41:91:94:1b:35:0d:e6:75:d7:46:a0:eb:
|
||||
ac:b2:98:ac:cb:af:6b:83:ce:8d:28:5b:c0:a3:7d:66:a9:dc:
|
||||
d9:f9:f4:c4:ce:88:c5:5f:fa:2d:4a:05:5f:37:70:ee:03:3e:
|
||||
79:ca:d5:e8:a0:f6:5c:3a:24:5b:18:29:89:4e:a9:79:6f:e6:
|
||||
2d:71:f1:42:8f:0f:96:7c:d6:07:51:a6:21:c2:17:dc:1d:b5:
|
||||
46:63:f8:26
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBzCCAe+gAwIBAgIUWrXuG0wVzwiLY4c5EmG8l8JVGa8wDQYJKoZIhvcNAQEL
|
||||
BQAwEzERMA8GA1UEAwwIcG9zdGdyZXMwHhcNMjQwNTAyMTI1NDQyWhcNMjQwNjAx
|
||||
MTI1NDQyWjATMREwDwYDVQQDDAhwb3N0Z3JlczCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBANTwa0DsRqINlBvqVPCAPzlLFjibwtej3QAD2D1ksGUDb0oP
|
||||
b5XB7Q8qkTP79/xHTFm2l2wJSua6ztLIHXuB0Bfk2UKFld8SHVnA87OFKHzOKd8j
|
||||
t0yid4rfL6zfQYQeBsB78PQcup6vvYdtixyPSQVa1rMGaIOqOJYnhTC9ghDp97Gj
|
||||
CU+vQj3IimNATqGV9C46gZXTF0HwIJ/pNytSl38RToaTb1caNdOTg8W2V+zz90Yc
|
||||
1Ca45BB32dB/S7WlYtSdfzrWfxucGCsrwjU7poDGyVIPBk1/2og4fiW8fBauP/x4
|
||||
2mt8KIv039kMPWSUKYKW6IJOkNXmptaJwusizK0CAwEAAaNTMFEwHQYDVR0OBBYE
|
||||
FGmtRSPASbREBS2kAZzv7Ar0+XiQMB8GA1UdIwQYMBaAFGmtRSPASbREBS2kAZzv
|
||||
7Ar0+XiQMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAF5dT2du
|
||||
O1e2Wo0d6NyMvimO9SlDSDTR46OqTpkStz3too48zf+C09BhmeNAHBSzh4ZDf+si
|
||||
vZX/ZOk/uuV0bmKSgBaD7PZuG1yOfmXufb9aGG5HfoZgcDY4nybUz/ENnSNDcrku
|
||||
Q0o6TbAEHW3m9Ct3w3Xr8kqsPYdbc8tn2uiCa9AbLlWT5MnmgOfuQdBQh4M/gapQ
|
||||
cZw+gbZSmkBM4ZcpyYteRMyq2UGRlBs1DeZ110ag66yymKzLr2uDzo0oW8CjfWap
|
||||
3Nn59MTOiMVf+i1KBV83cO4DPnnK1eig9lw6JFsYKYlOqXlv5i1x8UKPD5Z81gdR
|
||||
piHCF9wdtUZj+CY=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDU8GtA7EaiDZQb
|
||||
6lTwgD85SxY4m8LXo90AA9g9ZLBlA29KD2+Vwe0PKpEz+/f8R0xZtpdsCUrmus7S
|
||||
yB17gdAX5NlChZXfEh1ZwPOzhSh8zinfI7dMoneK3y+s30GEHgbAe/D0HLqer72H
|
||||
bYscj0kFWtazBmiDqjiWJ4UwvYIQ6fexowlPr0I9yIpjQE6hlfQuOoGV0xdB8CCf
|
||||
6TcrUpd/EU6Gk29XGjXTk4PFtlfs8/dGHNQmuOQQd9nQf0u1pWLUnX861n8bnBgr
|
||||
K8I1O6aAxslSDwZNf9qIOH4lvHwWrj/8eNprfCiL9N/ZDD1klCmCluiCTpDV5qbW
|
||||
icLrIsytAgMBAAECggEAEDjfpuLJL8PSjnzf6zGN/36jqvzcz2JNrhhc4zJ2SL/F
|
||||
/PGnmdRcC719seTme8rvu17wcrvTf7foFUFHE7dsQ8G8tndoV0XxQ/rx5fGFR9rJ
|
||||
8FSvfHBdJJCDuIsKmfbSo1vd268LnZa8XOKgyDR3RufodtuMX0l6xZ+toyywybpx
|
||||
mzmTFXASctCkPCIAd0jSJ3cGp3t/wBb29zq+yDAhsLgpzq7qL0jo59fvgYAksnnp
|
||||
BeupBUCG8KgjHcRyybleH1beNjPw7hjf384dN19/RsDM3VxGpPOkBeum2DWg59GO
|
||||
SsdQVYVL5VjYP+Lpd03d13JeNk8LvvvnE3/KrOs/OQKBgQD/bhgW+5HKFXxiqInJ
|
||||
r/TFaiMP/jmnxtGhLG+XOPuG1D36U1LfyONn7POc57dN4NYsuBnuvGbcHBAFnU/W
|
||||
AzbuB4RISYn06NmIJNFf0Nasm901b5yOYho1jgyXxDfg0WawUxIDfWZUa07RFhcF
|
||||
g2rrlLkyQimvcEeApLw7pfK4ZQKBgQDVag2nm8ANp0/vIlA/YCMDMZq2d00tt4+A
|
||||
8N5W/94+S9jnU7XU+8ubAsuR1PolLZsXy57wmP2PPtbS6QA7ta3tz5KuUYUw8MRi
|
||||
YaXLhJ9LBeQFbGF4gqRKSFfUFUZAQuj63z9GkBEqvEEEQFQE8LFoAmQEVROPStlN
|
||||
1atsCGSqqQKBgQDGxa4JdIvnbwD05aMUSNF6LzZJ49SKM7Y+bo4IaFeUfOYeP2lm
|
||||
rLYK0eJiYA/NkwIYmsvTdNm2wD/XnqvaJ3Y/lanjdxbcd7eQstYccjAc8s4aH1j9
|
||||
lGpA8uHp2NCDK3F2Fa8W+CcB2t/i5gXUTIiP+5SC/+nglMeP3KJ+6I0R4QKBgQDO
|
||||
YxCvfdU+nwaTmdiHYZ6akRcwzLErvDAgn1A2TJ4OGuW52N+pCJT/5LUf5BrBZ5hs
|
||||
a7Pas+nK5Rd1wnVCSVHaAPq2cAnhqYYSKqLe7fPq5th8UjL1C0p8t/eQF77nmI2d
|
||||
R9b2k462iHZL+TYHzQmUL3OwKOKQme8K8c8k2/0WeQKBgGK7HZRjDGoxlfLx0WLV
|
||||
b//gtdWIFD2rjRxMGMsvj+xRpK7xjz+5h3ULcEei13CgSlpTGtdV0B3WciTTJJMI
|
||||
JIEi23dzOGi3aj04W4VlQVpsGITVhb0Y5Q8R2jYt95xfETW2cu5kcCiee9B1UFjB
|
||||
y2Pz6+9ERVYaTkkHwfBZj2DK
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,62 @@
|
||||
Certificate Request:
|
||||
Data:
|
||||
Version: 1 (0x0)
|
||||
Subject: CN = postgres
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
Modulus:
|
||||
00:d4:f0:6b:40:ec:46:a2:0d:94:1b:ea:54:f0:80:
|
||||
3f:39:4b:16:38:9b:c2:d7:a3:dd:00:03:d8:3d:64:
|
||||
b0:65:03:6f:4a:0f:6f:95:c1:ed:0f:2a:91:33:fb:
|
||||
f7:fc:47:4c:59:b6:97:6c:09:4a:e6:ba:ce:d2:c8:
|
||||
1d:7b:81:d0:17:e4:d9:42:85:95:df:12:1d:59:c0:
|
||||
f3:b3:85:28:7c:ce:29:df:23:b7:4c:a2:77:8a:df:
|
||||
2f:ac:df:41:84:1e:06:c0:7b:f0:f4:1c:ba:9e:af:
|
||||
bd:87:6d:8b:1c:8f:49:05:5a:d6:b3:06:68:83:aa:
|
||||
38:96:27:85:30:bd:82:10:e9:f7:b1:a3:09:4f:af:
|
||||
42:3d:c8:8a:63:40:4e:a1:95:f4:2e:3a:81:95:d3:
|
||||
17:41:f0:20:9f:e9:37:2b:52:97:7f:11:4e:86:93:
|
||||
6f:57:1a:35:d3:93:83:c5:b6:57:ec:f3:f7:46:1c:
|
||||
d4:26:b8:e4:10:77:d9:d0:7f:4b:b5:a5:62:d4:9d:
|
||||
7f:3a:d6:7f:1b:9c:18:2b:2b:c2:35:3b:a6:80:c6:
|
||||
c9:52:0f:06:4d:7f:da:88:38:7e:25:bc:7c:16:ae:
|
||||
3f:fc:78:da:6b:7c:28:8b:f4:df:d9:0c:3d:64:94:
|
||||
29:82:96:e8:82:4e:90:d5:e6:a6:d6:89:c2:eb:22:
|
||||
cc:ad
|
||||
Exponent: 65537 (0x10001)
|
||||
Attributes:
|
||||
(none)
|
||||
Requested Extensions:
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Signature Value:
|
||||
9d:03:cd:5f:bd:9a:b2:db:dc:89:1c:3e:42:f3:6e:14:e3:d0:
|
||||
85:c7:db:53:75:77:3a:9a:1b:e5:f1:e0:5c:f9:cf:96:6d:71:
|
||||
fd:82:21:07:7c:d9:ce:60:d5:05:56:5e:d8:03:d4:e6:21:2e:
|
||||
e6:39:46:e1:6a:0a:d2:1c:48:a6:5d:be:52:ca:71:ce:b1:43:
|
||||
38:aa:2e:11:77:06:b9:5a:b7:96:54:c3:b3:54:ee:33:2f:de:
|
||||
b2:c0:22:01:07:79:50:42:10:1d:e8:cb:ff:51:fa:0e:cc:f2:
|
||||
a6:57:fb:8d:d0:02:1a:0c:48:58:90:82:82:70:ef:a6:f3:81:
|
||||
1a:7d:dc:49:49:db:cf:88:28:f5:64:20:c3:99:4c:33:37:59:
|
||||
d8:9e:fd:56:2e:77:e8:c8:b2:36:1b:8a:95:85:d1:81:7b:db:
|
||||
cc:f2:64:38:3f:59:62:ef:77:44:57:bc:3f:53:2c:ad:0e:88:
|
||||
ad:5d:83:24:f4:61:df:56:0a:cc:37:82:65:60:d1:d0:ad:2c:
|
||||
51:0b:9a:6d:02:83:c5:f1:67:95:b0:99:44:e6:64:47:ef:ac:
|
||||
b6:20:57:ea:b2:be:52:b3:e7:01:d1:69:8c:5a:a0:11:78:6b:
|
||||
45:0b:ad:7b:93:1c:77:c7:7b:c7:43:16:6c:82:31:2b:45:a2:
|
||||
28:64:30:91
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICWDCCAUACAQAwEzERMA8GA1UEAwwIcG9zdGdyZXMwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDU8GtA7EaiDZQb6lTwgD85SxY4m8LXo90AA9g9ZLBl
|
||||
A29KD2+Vwe0PKpEz+/f8R0xZtpdsCUrmus7SyB17gdAX5NlChZXfEh1ZwPOzhSh8
|
||||
zinfI7dMoneK3y+s30GEHgbAe/D0HLqer72HbYscj0kFWtazBmiDqjiWJ4UwvYIQ
|
||||
6fexowlPr0I9yIpjQE6hlfQuOoGV0xdB8CCf6TcrUpd/EU6Gk29XGjXTk4PFtlfs
|
||||
8/dGHNQmuOQQd9nQf0u1pWLUnX861n8bnBgrK8I1O6aAxslSDwZNf9qIOH4lvHwW
|
||||
rj/8eNprfCiL9N/ZDD1klCmCluiCTpDV5qbWicLrIsytAgMBAAGgADANBgkqhkiG
|
||||
9w0BAQsFAAOCAQEAnQPNX72astvciRw+QvNuFOPQhcfbU3V3Opob5fHgXPnPlm1x
|
||||
/YIhB3zZzmDVBVZe2APU5iEu5jlG4WoK0hxIpl2+UspxzrFDOKouEXcGuVq3llTD
|
||||
s1TuMy/essAiAQd5UEIQHejL/1H6Dszyplf7jdACGgxIWJCCgnDvpvOBGn3cSUnb
|
||||
z4go9WQgw5lMMzdZ2J79Vi536MiyNhuKlYXRgXvbzPJkOD9ZYu93RFe8P1MsrQ6I
|
||||
rV2DJPRh31YKzDeCZWDR0K0sUQuabQKDxfFnlbCZROZkR++stiBX6rK+UrPnAdFp
|
||||
jFqgEXhrRQute5Mcd8d7x0MWbIIxK0WiKGQwkQ==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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!'
|
||||
# DATABASE_URL='Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;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: "." }
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- .:/var/www
|
||||
depends_on:
|
||||
# mssql: { condition: service_healthy }
|
||||
[ "${COMPOSE_PROFILES-postgres}" ]
|
||||
environment:
|
||||
DATABASE_URL: ${COMPOSE_PROFILES-postgres}://root:Password123!@${COMPOSE_PROFILES:-postgres}/sqlpage${DATABASE_URL_PARAMS:-}
|
||||
RUST_LOG: sqlpage=trace
|
||||
postgres:
|
||||
profiles: ["postgres"]
|
||||
ports: ["5432:5432"]
|
||||
build: { context: "db-test-setup/postgres" }
|
||||
shm_size: 128mb
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_DB: sqlpage
|
||||
POSTGRES_PASSWORD: Password123!
|
||||
healthcheck:
|
||||
test: pg_isready -U root -d sqlpage
|
||||
|
||||
mysql:
|
||||
profiles: ["mysql"]
|
||||
ports: ["3306:3306"]
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: Password123!
|
||||
MYSQL_DATABASE: sqlpage
|
||||
|
||||
mssql:
|
||||
profiles: ["mssql"]
|
||||
ports: ["1433:1433"]
|
||||
build: { context: "db-test-setup/mssql" }
|
||||
healthcheck:
|
||||
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U root -P "Password123!" -Q "SELECT 1" -b -o /dev/null -No
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
mariadb:
|
||||
profiles: ["mariadb"]
|
||||
ports: ["3306:3306"]
|
||||
image: mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: Password123!
|
||||
MYSQL_DATABASE: sqlpage
|
||||
|
||||
oracle:
|
||||
profiles: ["oracle"]
|
||||
ports: ["1521:1521"]
|
||||
image: gvenzl/oracle-free:slim
|
||||
environment:
|
||||
ORACLE_PASSWORD: Password123!
|
||||
APP_USER: root
|
||||
APP_USER_PASSWORD: Password123!
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 640 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 188 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" fill="none" stroke="#e7f6f8" stroke-linecap="round" stroke-linejoin="round" stroke-width="75">
|
||||
<defs>
|
||||
<filter id="b" width="200%" height="200%" x="-50%" y="-50%">
|
||||
<feGaussianBlur stdDeviation="45"/>
|
||||
</filter>
|
||||
<path id="a" d="M171 264a341 122 0 1 0 682 0 341 122 0 1 0-682 0m0 0v245a341 122 0 0 0 682 0V264M171 509v244a341 122 0 0 0 682 0V509"/>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#0f3953" stroke="none" ry="64"/>
|
||||
<use href="#a"/>
|
||||
<use filter="url(#b)" href="#a"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 275 KiB |
@@ -0,0 +1,57 @@
|
||||
# **SQLPage** : Build a web application with a few SQL queries
|
||||
|
||||
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-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.
|
||||
|
||||
## Introduction
|
||||
|
||||
SQLPage opens the world of easy web application development to database specialists. Built with the new capabilities of modern database software in mind, SQLPage makes it quick and easy to build user interfaces on top of databases, without locking you into a proprietary system: it's all plain old _standard_ SQL. Say goodbye to complex frameworks and proprietary ecosystems – SQLPage offers a refreshing approach that unlocks the full potential of PostgreSQL.
|
||||
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Database-Centric Approach**: SQLPage keeps your database at the center of your application, preserving data integrity and leveraging PostgreSQL's rich functionality.
|
||||
- **Rapid Prototyping**: Develop a minimum viable product (MVP) in a matter of hours, allowing you to quickly validate your ideas and iterate on them.
|
||||
- **Full SQL support**: SQLPage is not limited to building read-only web views of your database; it supports inserting, updating and deleting data easily.
|
||||
- **Seamless Integration**: SQLPage seamlessly connects to your PostgreSQL database, leveraging its robustness, performance, and scalability.
|
||||
- **Component-Based UI**: Create beautiful and interactive user interfaces without torturing yourself with CSS using pre-built web components, providing a professional look and feel.
|
||||
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Internal Dashboards**: Build data-driven dashboards and reporting tools that empower teams to make informed decisions.
|
||||
- **Business Intelligence Apps**: Develop powerful business intelligence applications with intuitive interfaces for data exploration and analysis.
|
||||
- **Rapid Prototyping**: Validate and iterate on your ideas quickly by rapidly creating functional _minimum viable products_.
|
||||
- **Admin Interfaces**: Construct efficient and user-friendly administrative interfaces for managing and interacting with your PostgreSQL data.
|
||||
|
||||
## 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-page.com)
|
||||
|
||||
```
|
||||
SELECT 'list' AS component, 'components' AS title;
|
||||
```
|
||||
|
||||
```
|
||||
SELECT
|
||||
name AS title,
|
||||
description,
|
||||
icon,
|
||||
'?component='||name||'#component' AS link,
|
||||
$component = name AS active
|
||||
from component
|
||||
order by name;
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
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/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!
|
||||
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 427 KiB |
@@ -0,0 +1,45 @@
|
||||
# 📢 Announcing SQLPage: Build Dynamic SQLite Applications in SQL
|
||||
|
||||
Hello everyone !
|
||||
|
||||
I'm not sure whether announcements like this are allowed here; feel free to delete this post if they are not.
|
||||
|
||||
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-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-page.com)
|
||||
|
||||
[](https://github.com/sqlpage/SQLPage)
|
||||
|
||||
|
||||
## ❓ What is it ?
|
||||
|
||||
It is a small opensource web server distributed as a single binary that executes your `.sql` files, and renders the results using nice web components (tables, lists, forms, plots, ...).
|
||||
|
||||
Of course, if you are making a huge application with a complex business logic, SQLPage is not for you. But if you have a SQLite database lying around that you would want to share access to through a nice UI without spending too much time on it, you should try it.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **🗄️ SQL-only**: Create full web applications with a sleek frontend without touching HTML, CSS, or JavaScript.
|
||||
- **📝 Full SQL Support**: Auto-generated Web UI. Write only raw SQL queries.
|
||||
- **🔄 Integrated**: Supports any existing SQLite database, including using SQLlite extensions, leveraging its data using a standard .sql file.
|
||||
- **🌐 Web Standards Support**: Read and write HTTP cookies, manage user authentication, handle form submissions, and URL parameters.
|
||||
- **🚀🔒 Fast And Secure**: Written in Rust, ensuring no memory corruption, SQL injections, or XSS vulnerabilities.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Some cool things people are building with SQLPage:
|
||||
|
||||
- 📊 Internal Dashboards: Empower teams with data-driven insights.
|
||||
- 📈 Small Business Intelligence Apps: Build powerful applications for analysis and exploration.
|
||||
- 🗂️ Admin Interfaces: Manage and interact with SQLite data effectively.
|
||||
- 🎮 A Game: Rapidly validate and iterate on the idea.
|
||||
- 📚 Knowledge Management Tool: Replace an Excel file with a real database quickly
|
||||
|
||||
## Open-Source
|
||||
|
||||
- [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)
|
||||
@@ -0,0 +1,47 @@
|
||||
digraph {
|
||||
rankdir=LR;
|
||||
node [shape=rectangle, style=rounded, fontname=helvetica];
|
||||
edge [arrowhead=normal, arrowtail=crow, dir=both];
|
||||
|
||||
subgraph cluster_browser {
|
||||
label = "Web Browser";
|
||||
style = filled;
|
||||
color = lightgrey;
|
||||
node [style=filled, color=white];
|
||||
browser [label = "<f0>Web Browser|<f1>"];
|
||||
}
|
||||
|
||||
subgraph cluster_sqlpage {
|
||||
label = "SQLPage";
|
||||
style = filled;
|
||||
color = lightblue;
|
||||
node [style=filled, color=white];
|
||||
read_file [label = "<f0>Read File|<f1>todos.sql"];
|
||||
replace_param [label = "<f0>Replace Parameter|<f1>$todo_id with 1"];
|
||||
format_results [label = "<f0>Format Results|<f1>as HTML"];
|
||||
}
|
||||
|
||||
subgraph cluster_database {
|
||||
label = "Database";
|
||||
style = filled;
|
||||
color = palegreen;
|
||||
node [style=filled, color=white];
|
||||
execute_query [label = "<f0>Execute|<f1>Query"];
|
||||
return_results [label = "<f0>Return|<f1>Results"];
|
||||
}
|
||||
|
||||
browser:f1 -> http_request [label = " HTTP GET Request\n/todos.sql?todo_id=1", lhead=cluster_browser];
|
||||
http_request -> read_file:f0 [lhead=cluster_sqlpage];
|
||||
read_file:f1 -> replace_param:f0;
|
||||
replace_param:f1 -> sql_query [label = " SQL Query\nSELECT * FROM todos\nWHERE id = 1", lhead=cluster_sqlpage];
|
||||
sql_query -> execute_query:f0 [lhead=cluster_database];
|
||||
execute_query:f1 -> return_results:f0;
|
||||
return_results:f1 -> result_stream [label = " Result Stream", lhead=cluster_database];
|
||||
result_stream -> format_results:f0 [lhead=cluster_sqlpage];
|
||||
format_results:f1 -> html_response [label = " HTML Response\nwith Formatted Results", ltail=cluster_sqlpage];
|
||||
html_response -> browser:f0 [label = " Display Results", lhead=cluster_browser];
|
||||
|
||||
// Additional Styles
|
||||
edge [arrowhead=normal, arrowtail=none, dir=both, fontname=helvetica, fontsize=10];
|
||||
node [fontsize=12];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<mxfile host="app.diagrams.net" modified="2024-04-30T20:35:55.594Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0" etag="fCNrnm6Ivitl6KE4-js5" version="24.3.1" type="device">
|
||||
<diagram name="Page-1" id="qWvz8Z0mP-6tBl2-Tc15">
|
||||
<mxGraphModel dx="2267" dy="907" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-2" value="" style="rounded=1;whiteSpace=wrap;html=1;arcSize=6;" parent="1" vertex="1">
|
||||
<mxGeometry x="20" y="230" width="320" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-1" value="<h1 style="margin-top: 0px;">GET /todo.sql?<font color="#003300">id=1</font><br></h1><pre style="font-size: 13px;"><font style="font-size: 13px;">Host: example.com</font></pre><pre style="font-size: 13px;" data-signature="FRjGbP3zQQCt51JFaD0Ll1xKb2HQKZ0zWZPelzg69HE=" class="brush: http notranslate"><font style="font-size: 13px;"><code><span class="token header"><span class="token header-name keyword">Cookie</span><span class="token punctuation">:</span> <span class="token header-value">name=value</span></span></code></font></pre><p></p>" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="245" width="290" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-3" value="" style="curved=1;endArrow=classic;html=1;rounded=0;strokeWidth=5;dashed=1;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="370" y="320" as="sourcePoint" />
|
||||
<mxPoint x="390" y="700" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="320" />
|
||||
<mxPoint x="720" y="330" />
|
||||
<mxPoint x="800" y="480" />
|
||||
<mxPoint x="790" y="590" />
|
||||
<mxPoint x="710" y="710" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-4" value="" style="whiteSpace=wrap;html=1;shape=mxgraph.basic.document;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="230" width="140" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-5" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1">
|
||||
<mxGeometry x="677" y="420" width="150" height="195" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-6" value="" style="rounded=1;whiteSpace=wrap;html=1;arcSize=6;" parent="1" vertex="1">
|
||||
<mxGeometry x="25" y="600" width="320" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-7" value="<h1 style="margin-top: 0px;">HTTP/1.1 200 OK<br></h1><pre style="font-size: 13px;"><font style="font-size: 13px;">Server: SQLPage</font></pre><pre style="font-size: 13px;" data-signature="FRjGbP3zQQCt51JFaD0Ll1xKb2HQKZ0zWZPelzg69HE=" class="brush: http notranslate"><font style="font-size: 13px;"><code><span class="token header"><span class="token header-name keyword"><br>&lt;html&gt;&lt;head&gt;&lt;title&gt;My SQLPage...<br></span></span></code></font></pre><p></p>" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="45" y="615" width="290" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-8" value="<div>SELECT</div><div>&nbsp; 'list' AS component;<br></div><div><br></div><div><br></div><div>SELECT *</div><div>FROM todos</div><div>WHERE id =<font color="#003300"> $todo_id<br></font></div>" style="text;html=1;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="486" y="250" width="140" height="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-9" value="<div>list.handlebars</div><div><br></div><div>(pre-built component)<br></div>" style="whiteSpace=wrap;html=1;shape=mxgraph.basic.document" parent="1" vertex="1">
|
||||
<mxGeometry x="490" y="610" width="140" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-10" value="" style="shape=image;verticalLabelPosition=bottom;labelBackgroundColor=default;verticalAlign=top;aspect=fixed;imageAspect=0;image=https://upload.wikimedia.org/wikipedia/commons/2/29/Postgresql_elephant.svg;" parent="1" vertex="1">
|
||||
<mxGeometry x="713.5" y="490" width="77" height="79.41" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-11" value="Website User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="-440" y="410" width="80" height="140" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-15" value="" style="endArrow=classic;html=1;rounded=0;strokeWidth=10;fillColor=#f8cecc;strokeColor=#b85450;exitX=-0.061;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="-Mu83Rb6pQ0imwjiuAuA-16" target="-Mu83Rb6pQ0imwjiuAuA-8" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="677" y="220" as="sourcePoint" />
|
||||
<mxPoint x="637" y="270" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-16" value="You write this" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily= Just Another Hand ;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3D%2BJust%2BAnother%2BHand%2B;fontSize=24;fontColor=#FF3333;" parent="1" vertex="1">
|
||||
<mxGeometry x="700.5" y="170" width="90" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-17" value="" style="endArrow=classic;html=1;rounded=0;strokeWidth=10;fillColor=#e1d5e7;strokeColor=#9673a6;exitX=-0.061;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="224.5" y="538" as="sourcePoint" />
|
||||
<mxPoint x="155.5" y="585" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-18" value="" style="endArrow=classic;html=1;rounded=0;strokeWidth=10;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="217.25" y="433" as="sourcePoint" />
|
||||
<mxPoint x="152.75" y="390" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-19" value="" style="endArrow=classic;html=1;rounded=0;strokeWidth=10;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="450" y="550" as="sourcePoint" />
|
||||
<mxPoint x="525.5" y="600" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-20" value="" style="endArrow=classic;html=1;rounded=0;strokeWidth=10;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="520" y="490" as="sourcePoint" />
|
||||
<mxPoint x="675.5" y="485" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="-Mu83Rb6pQ0imwjiuAuA-21" value="All of this is handled for you automatically" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily= Just Another Hand ;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3D%2BJust%2BAnother%2BHand%2B;fontSize=24;fillColor=#f5f5f5;fontColor=#9577a3;strokeColor=#666666;" parent="1" vertex="1">
|
||||
<mxGeometry x="240" y="430" width="230" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-1" value="Request" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="20" y="200" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-2" value="<div style="font-size: 13px;" align="left">/todos.sql?todo_id=1</div>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;fontSize=13;fontFamily=Courier New;" vertex="1" parent="1">
|
||||
<mxGeometry x="97.5" y="250" width="185" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-3" value="URL" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontColor=#1f2f1e;" vertex="1" parent="1">
|
||||
<mxGeometry x="97.5" y="230" width="52.5" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-4" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;" vertex="1" parent="1">
|
||||
<mxGeometry x="185" y="255" width="85" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-5" value="URL Parameters" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=7;" vertex="1" parent="1">
|
||||
<mxGeometry x="185" y="242" width="75" height="15" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-6" value="" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.browserWindow;rSize=0;strokeColor=#666666;strokeColor2=#008cff;strokeColor3=#c4c4c4;mainText=,;recursiveResize=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="-330" y="380" width="330" height="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-7" value="Page 1" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;fontSize=17;fontColor=#666666;align=left;whiteSpace=wrap;" vertex="1" parent="67phEDKegGIg4GmPQhPB-6">
|
||||
<mxGeometry x="60" y="12" width="110" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-8" value="y.fr/todos.sql?todo_id=1" style="strokeWidth=1;shadow=0;dashed=0;align=center;html=1;shape=mxgraph.mockup.containers.anchor;rSize=0;fontSize=17;fontColor=#666666;align=left;" vertex="1" parent="67phEDKegGIg4GmPQhPB-6">
|
||||
<mxGeometry x="130" y="60" width="250" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-10" value="<h1 style="margin-top: 0px;">My SQLPage Website<br></h1><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="67phEDKegGIg4GmPQhPB-6">
|
||||
<mxGeometry x="20" y="120" width="290" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-9" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.332;exitY=0.078;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.001;entryY=0.476;entryDx=0;entryDy=0;entryPerimeter=0;fontColor=#789170;fillColor=#d5e8d4;strokeColor=#82b366;" edge="1" parent="1" source="67phEDKegGIg4GmPQhPB-8" target="-Mu83Rb6pQ0imwjiuAuA-2">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-260" y="340" as="sourcePoint" />
|
||||
<mxPoint x="-210" y="290" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="67phEDKegGIg4GmPQhPB-11" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.008;exitY=0.444;exitDx=0;exitDy=0;exitPerimeter=0;fillColor=#d5e8d4;strokeColor=#82b366;" edge="1" parent="1" source="-Mu83Rb6pQ0imwjiuAuA-6" target="67phEDKegGIg4GmPQhPB-10">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-230" y="730" as="sourcePoint" />
|
||||
<mxPoint x="-180" y="680" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
@@ -0,0 +1,292 @@
|
||||
This demo/template defines a basic CRUD application with authentication designed for use with SQLite. The primary goal of this demo is to explore some of the SQLPage features and figure out how to implement various aspects of a data-centric application. The template contains both public pages ([**SQLite Introspection**](www/intro.sql)) and pages requiring authentication. A user management GUI is not available (database migrations, included in the project, create a default login admin/admin).
|
||||
|
||||
The website root is set to _./www_, and the database is in the _./db/_ directory. The database contains one data table, "currencies", and its content is listed on a login-protected [**page**](www/currencies_list.sql). After successful authentication, you can see the list of records and access the form for adding/editing/deleting records.
|
||||
|
||||
## Authentication process
|
||||
|
||||
Three files (login.sql, logout.sql, and create_session.sql) implement authentication mostly following the code provided in other examples. The login.sql defines the actual login form, and the two other files do not have any associated GUI but perform appropriate processing and redirect to designated targets. Given a protected page, the general authentication flow is as follows.
|
||||
|
||||
1. The user attempts to open a protected page (e.g., currencies_list.sql)
|
||||
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 = (
|
||||
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):
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
sqlpage.link('/login.sql', json_object('path', $_curpath)) AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
4. The login page renders the login form, accepts the user credentials, and redirects to create_session.sql, passing the login credentials as POST variables.
|
||||
5. create_session.sql checks credentials. If this check fails, it redirects back to the login form. If the check succeeds, it generates a session token and performs the final redirect.
|
||||
|
||||
## Header module
|
||||
|
||||
### Controlling execution of parts in a loaded script
|
||||
|
||||
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;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
The second line above sets the local variable $\_session_required, which indicates whether authentication is required for a particular page. This variable, like the GET/POST variables, is then accessible to the loaded "header" module header_shell_session.sql. This way, if other common code is placed in the header module, it can be executed by a non-protected page while skipping the authentication part (by setting $\_session_required = 0, which prevents redirect to the login form even if no valid session token is available).
|
||||
|
||||
Another "filter" variable (`$_shell_enabled`) controls the execution of another section in the header module, as discussed below.
|
||||
|
||||
### Tracking the calling page
|
||||
|
||||
The first line above sets another useful variable $\_curpath, which makes it possible to redirect back to the starting page after the authentication process is completed, rather than redirecting to the front page. The loaded header module has access to this variable as well, and if a login redirect is required, this variable is passed alone as a GET URL parameter (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
|
||||
The login form passes it further in a similar fashion to the create_session.sql script (as part of the "action" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'form' AS component,
|
||||
'Login' AS title,
|
||||
'create_session.sql' || ifnull('?path=' || $path, '') AS action;
|
||||
```
|
||||
If authentication fails, create_session.sql redirects back to login.sql and makes sure to pass the $path value alone (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || $path, '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM "accounts"
|
||||
WHERE username = :username) AS password_hash;
|
||||
```
|
||||
|
||||
If authentication succeeds, create_session.sql redirects back to starting page using the $path value as the final redirect target:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
```
|
||||
|
||||
### Adding User/Login/Logout buttons to the page menu
|
||||
|
||||
It is customary to show the "User Profile" button in the top right corner when the user is authenticated. Also, it is customary to show the "Logout" button next to the "User Profile" button or the "Login" button when no active session is available. The associated code is common to all pages, and it makes sense to place it in the same header module.
|
||||
|
||||
The "shell" component is responsible for constructing the top menu, but the standard component does not support menu buttons. The simplest solution to this "limitation" is to modify the standard shell.handlebars template found in the "sqlpage/templates" directory of the SQLPage source code repository and place it inside the project "sqlpage/templates" directory.
|
||||
|
||||
To extend the "shell" component with button items in the menu, I have added a hybrid section of code mostly constructed from template code defining menu items and the "button" component. In the present implementation, menu buttons are defined as a JSON array value to the "menu_buttons" property. Each array member is a JSON object defining a single button and may include "shape", "color", "size", "outline", "link", "tooltip", and "title" properties (see description of these properties in the official "button" component docs.
|
||||
|
||||
Note how the `$_curpath` variable, which is set in core page modules (such as currencies_list.sql) is used to define links for the Login/Logout buttons. These links are irrelevant for protected pages, but for non-protected pages, such as intro.sql, these links make sure that the user remains on the same page after he/she presses on Logout/Login buttons (and completes authentication in the latter case).
|
||||
|
||||
The `$_username` variable set during the authentication process is then used to decide which buttons (Login or User/Logout) should be shown.
|
||||
|
||||
The `$_shell_enabled` variable controls the execution of the custom shell component. This feature is necessary because the header module is also loaded by the currencies_item_dml.sql module, which should only be accessible to authenticated users. However, the currencies_item_dml.sql module is a no-GUI module, which performs database operations and uses redirects after the requested operations are completed. At the same time, if the loaded header module executes the custom shell component, generating GUI buttons, the redirection mechanism in currencies_item_dml.sql will fail.
|
||||
|
||||
### Required variable guards
|
||||
|
||||
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);
|
||||
```
|
||||
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.
|
||||
|
||||
## Footer module - debug information
|
||||
|
||||
POST/GET/SET variables may provide helpful information for debugging purposes. In earlier [post](https://reddit.com/r/SQLpage/comments/1dh1siw/structuring_code_showing_debug_info/), I described the code I use to output variables in a convenient way. Briefly, I use `sqlpage.variables('GET')` and `sqlpage.variables('POST')` to get all variables, and I distinguish between the GET variables and local SET variables by prefixing SET variable names with an underscore. Initially, I copy-pasted the code snippets at the bottom of pages, but later I moved it to a separate file, footer_debug_post-get-set.sql, which I load via
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
```
|
||||
## Structuring code modules
|
||||
|
||||
The "currencies" table is handled by three modules:
|
||||
|
||||
- "table" view - __currencies_list.sql__
|
||||
Displays the entire table using the powerful "table" component. One way to extend this module is, possibly, to hide certain less important columns, especially for wide tables.
|
||||
- "detail" view - __currencies_item_form.sql__
|
||||
This is the "detail" view. It shows all fields for a single record. In this case, it is an "editable" form, though the fields maybe made conditionally read-only. Another possible option for a read-only detail view is to use the "datagrid" component.
|
||||
- database DML processor - __currencies_item_dml.sql__
|
||||
This is a no-GUI module, which only processes database modification operations using data submitted to the currencies_item_form.sql form. Presently, all DML statements (INSERT/UPDATE/DELETE) are processed by this module. If necessary, this module maybe split into more specialized modules.
|
||||
|
||||
Let us briefly go over the code block in these modules.
|
||||
|
||||
### Debug information (bottom section)
|
||||
|
||||
All three module load the footer module discussed above that produces a conditional output of GET/POST/SET variables.
|
||||
|
||||
### Authentication (top section)
|
||||
|
||||
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;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
This code discussed above sets the current path variable (necessary for correct redirects), authentication flag before loading the header module that takes care of authentication and common settings, such as top menu buttons.
|
||||
|
||||
The "no-GUI" currencies_item_dml.sql module does not set `$_curpath`, since it cannot be a start/end point in a redirect chain, but it sets the `$_shell_enabled` flag to suppress top menu buttons generation, as discussed earlier.
|
||||
|
||||
### Common variables
|
||||
|
||||
The second section may generally be used to set additional common variables, such as the name of the "table" view inside the "detail" view and the other way around (to switch between the two).
|
||||
|
||||
The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by the "table" view). This way, if a record is modified/deleted starting from, e.g., the "table" view, the same view is set as the final redirect target after the DML operation is completed.
|
||||
|
||||
### Table view
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### Detail view
|
||||
|
||||
The detail view module is more elaborate. If "&id" GET URL parameter is provided, the form shows the corresponding record. Otherwise, the ID field is rendered as a dropdown list populated from the database, but is set to NULL. The remaining fields are either blank or contain dummy values.
|
||||
|
||||
The first step (after the previously discussed common sections), therefore, is to filter invalid id values.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
WHERE $id = '' OR CAST($id AS INT) = 0;
|
||||
|
||||
set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided');
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
```
|
||||
|
||||
The blank string and zero are considered the equivalents of NULL, so redirect to itself is activated, removing the id parameter. If no id is provided or id is set to an integer value, the first check does not trigger. The second check above triggers when there is no record with provided id. This check resets id and displays an error message.
|
||||
|
||||
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 =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
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 = (
|
||||
WITH
|
||||
fields AS (
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
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 = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The next part defines form fields via the "dynamic" component (for some reason I am having issues with POST variables when the form is defined directly via the "form" component. Note how the $values variable prepared in previous blocks is used to populate the form. Without the SET statement, everything would need to be incorporated in a single query (which is feasible thanks to CTEs, but would still be significantly more difficult to develop and maintain).
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed.
|
||||
|
||||
The last big section defines the main form buttons, which are adjusted based on the type of operation (similarly to the form fields above).
|
||||
|
||||
The final section includes a general confirmation alert (used after INSERT/UPDATE operations) and an error alert.
|
||||
|
||||
### Coding style conventions
|
||||
|
||||
Consistent code style is important for code readability. Because SQLPage module maybe a mix of SQL code and sizeable text fragments, which may contain plain text, Markdown, JSON, HTML, etc., it might be difficult to follow a fixed set of rules. In fact, dynamically generated webpages regardless of specific technologies used tend to get messy. At the very least I strive to
|
||||
|
||||
- keep all SQL keywords always in the UPPER case,
|
||||
- have reasonably sensible code alignment (though some alignment approaches may not be generally advisable)
|
||||
- keep large static text pieces in separate appropriate dedicated files and load them via `sqlpage.read_file_as_text()` (e.g., the text of this file comes from Readme.md, where it can be properly edited by any Markdown editor and version-controlled; similarly, static JSON should go in \*.json files or in a dedicated database table with a designated JSON column).
|
||||
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
web:
|
||||
image: lovasoa/sqlpage:main
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./www:/var/www
|
||||
- ./sqlpage:/etc/sqlpage
|
||||
environment:
|
||||
DATABASE_URL: sqlite:///tmp/components.db?mode=rwc
|
||||
@@ -0,0 +1,39 @@
|
||||
DROP TABLE IF EXISTS _sqlx_migrations;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
description TEXT COLLATE NOCASE NOT NULL,
|
||||
installed_on TEXT COLLATE NOCASE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
success INTEGER NOT NULL,
|
||||
checksum BLOB NOT NULL,
|
||||
execution_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
|
||||
-- The path field should be relative to the www root. Do not
|
||||
-- include absolute paths pointing to files outside the www root.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sqlpage_files (
|
||||
path TEXT COLLATE NOCASE NOT NULL UNIQUE
|
||||
GENERATED ALWAYS AS (
|
||||
iif(prefix IS NOT NULL AND length(prefix) > 0, prefix || '/', '') ||
|
||||
name
|
||||
),
|
||||
contents BLOB,
|
||||
last_modified TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
prefix TEXT COLLATE NOCASE NOT NULL DEFAULT '',
|
||||
name TEXT COLLATE NOCASE NOT NULL,
|
||||
tag TEXT COLLATE NOCASE,
|
||||
src_url TEXT COLLATE NOCASE,
|
||||
PRIMARY KEY(prefix, name)
|
||||
);
|
||||
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS sqlpage_files_update
|
||||
AFTER UPDATE OF path, contents ON sqlpage_files
|
||||
WHEN old.last_modified = new.last_modified
|
||||
BEGIN
|
||||
UPDATE sqlpage_files
|
||||
SET last_modified = CURRENT_TIMESTAMP
|
||||
WHERE last_modified = new.last_modified;
|
||||
END;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS "accounts" (
|
||||
"username" TEXT COLLATE NOCASE PRIMARY KEY,
|
||||
"password_hash" TEXT COLLATE BINARY NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||
"id" TEXT COLLATE NOCASE PRIMARY KEY,
|
||||
"username" TEXT COLLATE NOCASE NOT NULL
|
||||
REFERENCES "accounts"("username"),
|
||||
"created_at" TEXT COLLATE NOCASE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
-- Creates an initial user with the username `admin` and the password `admin` (hashed using sqlpage.hash_password('admin'))
|
||||
|
||||
INSERT OR IGNORE INTO "accounts"("username", "password_hash") VALUES
|
||||
('admin', '$argon2id$v=19$m=19456,t=2,p=1$4lu3hSvaqXK0dMCPZLOIPg$PUFJSB6L3J5eZ33z9WX7y0nOH6KawV2FdW0abMuPE7o');
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "currencies" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT COLLATE NOCASE NOT NULL UNIQUE,
|
||||
"to_rub" REAL NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO "currencies"("name", "to_rub") VALUES
|
||||
('RUR', 1),
|
||||
('USD', 90),
|
||||
('CNY', 12.34);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"listen_on": "0.0.0.0:8080",
|
||||
"database_url": "sqlite://./db/Components.sqlite?mode=rwc",
|
||||
"allow_exec": false,
|
||||
"web_root": "./www"
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{language}}" style="font-size: {{default font_size 18}}px" {{#if class}}class="{{class}}"{{/if}}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{{default title "SQLPage"}}</title>
|
||||
{{#if favicon}}
|
||||
<link rel="icon" href="{{favicon}}">
|
||||
{{/if}}
|
||||
{{#if manifest}}
|
||||
<link rel="manifest" href="{{manifest}}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{static_path 'sqlpage.css'}}">
|
||||
{{#each (to_array css)}}
|
||||
{{#if this}}
|
||||
<link rel="stylesheet" href="{{this}}">
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if font}}
|
||||
{{#if (startsWith font "/")}}
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'LocalFont';
|
||||
src: url('{{font}}') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'LocalFont', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
{{else}}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={{font}}&display=fallback">
|
||||
<style>:root { --tblr-font-sans-serif: '{{font}}', Arial, sans-serif;}</style>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<script src="{{static_path 'sqlpage.js'}}" defer></script>
|
||||
{{#each (to_array javascript)}}
|
||||
{{#if this}}
|
||||
<script src="{{this}}" defer></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="description" content="{{description}}"/>
|
||||
{{#if norobot}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
{{/if}}
|
||||
|
||||
{{#if refresh}}
|
||||
<meta http-equiv="refresh" content="{{refresh}}">
|
||||
{{/if}}
|
||||
{{#if rss}}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{title}}" href="{{rss}}">
|
||||
{{/if}}
|
||||
<meta name="generator" content="SQLPage"/>
|
||||
|
||||
{{#if social_image}}
|
||||
<meta property="og:image" content="{{social_image}}"/>
|
||||
{{/if}}
|
||||
</head>
|
||||
|
||||
<body class="layout-{{default layout 'boxed'}}" {{#if theme}}data-bs-theme="{{theme}}"{{/if}}>
|
||||
<div class="page">
|
||||
{{#if title}}
|
||||
<header id="sqlpage_header">
|
||||
<nav class="navbar navbar-expand-md navbar-light{{#if fixed_top_menu}} fixed-top{{/if}}">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand flex-grow-1 overflow-hidden" href="{{#if link}}{{link}}{{else}}/{{/if}}">
|
||||
{{#if image}}
|
||||
<img src="{{image}}" alt="{{title}}" width="32" height="32"
|
||||
class="navbar-brand-image">
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
{{~icon_img icon~}}
|
||||
{{/if}}
|
||||
<h1 class="mb-0 w-0 fs-2">{{title}}</h1>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar-menu">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{{#each (to_array menu_item)}}
|
||||
{{#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}}
|
||||
{{#with (parse_json this)}}
|
||||
{{#if (or (gt (len this.title) 0) (gt (len this.icon) 0) (gt (len this.image) 0))}}
|
||||
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}{{#if this.button}} px-2{{/if}}">
|
||||
<a class="
|
||||
{{~#if this.button~}}
|
||||
btn text-wrap me-1
|
||||
{{~#if this.color}} btn-{{this.color}}{{/if}}
|
||||
{{~#if this.size}} btn-{{this.size}}{{/if}}
|
||||
{{~#if this.outline}} btn-outline-{{this.outline}}{{/if}}
|
||||
{{~#if this.shape}} btn-{{this.shape}}{{/if}}
|
||||
{{~#if this.narrow}} btn-icon{{/if}}
|
||||
{{~else~}}
|
||||
nav-link
|
||||
{{~/if}}
|
||||
{{~#if this.submenu}} dropdown-toggle{{/if}}"
|
||||
href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
|
||||
{{#if this.submenu}}data-bs-toggle="dropdown" data-bs-auto-close="outside"{{/if}}
|
||||
role="button"
|
||||
>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{this.title}}
|
||||
</a>
|
||||
{{#if this.submenu}}
|
||||
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static" style="min-width: inherit;">
|
||||
{{#each this.submenu}}
|
||||
{{#if (or (gt (len this.title) 0) (gt (len this.icon) 0) (gt (len this.image) 0))}}
|
||||
{{~#if this.button~}}<div class="mx-2" style="text-align: center;">{{/if}}
|
||||
<a class="
|
||||
{{~#if this.button~}}
|
||||
btn text-wrap my-1
|
||||
{{~#if this.color}} btn-{{this.color}}{{/if}}
|
||||
{{~#if this.size}} btn-{{this.size}}{{/if}}
|
||||
{{~#if this.outline}} btn-outline-{{this.outline}}{{/if}}
|
||||
{{~#if this.shape}} btn-{{this.shape}}{{/if}}
|
||||
{{~#if this.narrow}} btn-icon{{/if}}
|
||||
{{~else~}}
|
||||
dropdown-item
|
||||
{{~/if}}" style=" min-width: inherit;"
|
||||
{{~#if tooltip}} data-bs-toggle="tooltip" data-bs-placement="top" title="{{tooltip}}"{{/if}}
|
||||
href="{{this.link}}"
|
||||
>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{this.title}}
|
||||
</a>
|
||||
{{~#if this.button~}}</div>{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{~else}}
|
||||
{{~#if (gt (len this) 0)~}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-capitalize" href="{{this}}.sql">{{this}}</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if search_target}}
|
||||
<form class="d-flex" role="search" action="{{search_target}}">
|
||||
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" value="{{search_value}}">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
{{/if}}
|
||||
<main class="page-wrapper container-xl pt-3 px-md-5 px-sm-3 {{#if fixed_top_menu}} mt-5{{/if}}" id="sqlpage_main_wrapper">
|
||||
{{~#each_row~}}{{~/each_row~}}
|
||||
</main>
|
||||
</div>
|
||||
<footer class="w-100 text-center fs-6 my-2 text-secondary" id="sqlpage_footer">
|
||||
{{#if footer}}
|
||||
{{{markdown footer}}}
|
||||
{{else}}
|
||||
<!-- You can change this footer using the 'footer' parameter of the 'shell' component -->
|
||||
Built with <a class="text-reset" href="https://sql-page.com" title="SQLPage v{{buildinfo 'CARGO_PKG_VERSION'}}">SQLPage</a>
|
||||
{{/if}}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
# Public homepage is reachable without a session.
|
||||
GET http://localhost:8080
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
header "Content-Type" contains "text/html"
|
||||
body contains "Demo/Template CRUD with Authentication"
|
||||
body not contains "An error occurred"
|
||||
|
||||
# The currencies CRUD page is protected.
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
|
||||
GET http://localhost:8080/login.sql?path=/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Login"
|
||||
body contains "Username"
|
||||
body contains "Password"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/create_session.sql?path=/currencies_list.sql
|
||||
[FormParams]
|
||||
username: admin
|
||||
password: wrong
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "login.sql"
|
||||
header "Location" contains "error=1"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
|
||||
# Demo credentials from the example migration: admin/admin.
|
||||
POST http://localhost:8080/create_session.sql?path=/currencies_list.sql
|
||||
[FormParams]
|
||||
username: admin
|
||||
password: admin
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Set-Cookie" contains "session_token="
|
||||
header "Location" == "/currencies_list.sql"
|
||||
|
||||
# Hurl's cookie store reuses the session_token set above.
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currencies"
|
||||
body contains "New Record"
|
||||
body contains "RUR"
|
||||
body contains "USD"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?id=1
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currency"
|
||||
body contains "Exchange Rate to RUR"
|
||||
body contains "RUR"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?action=INSERT
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currency"
|
||||
body contains "Exchange Rate to RUR"
|
||||
body contains "BROWSE"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=UPDATE
|
||||
[FormParams]
|
||||
id:
|
||||
name: HURLTEST
|
||||
to_rub: 7.89
|
||||
HTTP 302
|
||||
[Captures]
|
||||
currency_id: header "Location" regex "id=(\\d+)"
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "info=INSERT"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "HURLTEST"
|
||||
body contains "7.89"
|
||||
body htmlUnescape contains "currencies_item_form.sql?&path=/currencies_list.sql&id={{currency_id}}"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=UPDATE
|
||||
[FormParams]
|
||||
id: {{currency_id}}
|
||||
name: HURLEDITED
|
||||
to_rub: 8.91
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "id={{currency_id}}"
|
||||
header "Location" contains "info=UPDATE"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?id={{currency_id}}
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "HURLEDITED"
|
||||
body contains "8.91"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=DELETE
|
||||
[FormParams]
|
||||
id: {{currency_id}}
|
||||
name: HURLEDITED
|
||||
to_rub: 8.91
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "info=DELETE"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body not contains "HURLEDITED"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/logout.sql?path=currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "currencies_list.sql"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
@@ -0,0 +1,292 @@
|
||||
This demo/template defines a basic CRUD application with authentication designed for use with SQLite. The primary goal of this demo is to explore some of the SQLPage features and figure out how to implement various aspects of a data-centric application. The template contains both public pages ([**SQLite Introspection**](intro.sql)) and pages requiring authentication. A user management GUI is not available (database migrations, included in the project, create a default login admin/admin).
|
||||
|
||||
The website root is set to _./www_, and the database is in the _./db/_ directory. The database contains one data table, "currencies", and its content is listed on a login-protected [**page**](currencies_list.sql). After successful authentication, you can see the list of records and access the form for adding/editing/deleting records.
|
||||
|
||||
## Authentication process
|
||||
|
||||
Three files (login.sql, logout.sql, and create_session.sql) implement authentication mostly following the code provided in other examples. The login.sql defines the actual login form, and the two other files do not have any associated GUI but perform appropriate processing and redirect to designated targets. Given a protected page, the general authentication flow is as follows.
|
||||
|
||||
1. The user attempts to open a protected page (e.g., currencies_list.sql)
|
||||
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 = (
|
||||
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):
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
4. The login page renders the login form, accepts the user credentials, and redirects to create_session.sql, passing the login credentials as POST variables.
|
||||
5. create_session.sql checks credentials. If this check fails, it redirects back to the login form. If the check succeeds, it generates a session token and performs the final redirect.
|
||||
|
||||
## Header module
|
||||
|
||||
### Controlling execution of parts in a loaded script
|
||||
|
||||
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;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
The second line above sets the local variable $\_session_required, which indicates whether authentication is required for a particular page. This variable, like the GET/POST variables, is then accessible to the loaded "header" module header_shell_session.sql. This way, if other common code is placed in the header module, it can be executed by a non-protected page while skipping the authentication part (by setting $\_session_required = 0, which prevents redirect to the login form even if no valid session token is available).
|
||||
|
||||
Another "filter" variable (`$_shell_enabled`) controls the execution of another section in the header module, as discussed below.
|
||||
|
||||
### Tracking the calling page
|
||||
|
||||
The first line above sets another useful variable $\_curpath, which makes it possible to redirect back to the starting page after the authentication process is completed, rather than redirecting to the front page. The loaded header module has access to this variable as well, and if a login redirect is required, this variable is passed alone as a GET URL parameter (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
|
||||
The login form passes it further in a similar fashion to the create_session.sql script (as part of the "action" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'form' AS component,
|
||||
'Login' AS title,
|
||||
'create_session.sql' || ifnull('?path=' || $path, '') AS action;
|
||||
```
|
||||
If authentication fails, create_session.sql redirects back to login.sql and makes sure to pass the $path value alone (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || $path, '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM "accounts"
|
||||
WHERE username = :username) AS password_hash;
|
||||
```
|
||||
|
||||
If authentication succeeds, create_session.sql redirects back to starting page using the $path value as the final redirect target:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
```
|
||||
|
||||
### Adding User/Login/Logout buttons to the page menu
|
||||
|
||||
It is customary to show the "User Profile" button in the top right corner when the user is authenticated. Also, it is customary to show the "Logout" button next to the "User Profile" button or the "Login" button when no active session is available. The associated code is common to all pages, and it makes sense to place it in the same header module.
|
||||
|
||||
The "shell" component is responsible for constructing the top menu, but the standard component does not support menu buttons. The simplest solution to this "limitation" is to modify the standard shell.handlebars template found in the "sqlpage/templates" directory of the SQLPage source code repository and place it inside the project "sqlpage/templates" directory.
|
||||
|
||||
To extend the "shell" component with button items in the menu, I have added a hybrid section of code mostly constructed from template code defining menu items and the "button" component. In the present implementation, menu buttons are defined as a JSON array value to the "menu_buttons" property. Each array member is a JSON object defining a single button and may include "shape", "color", "size", "outline", "link", "tooltip", and "title" properties (see description of these properties in the official "button" component docs.
|
||||
|
||||
Note how the `$_curpath` variable, which is set in core page modules (such as currencies_list.sql) is used to define links for the Login/Logout buttons. These links are irrelevant for protected pages, but for non-protected pages, such as intro.sql, these links make sure that the user remains on the same page after he/she presses on Logout/Login buttons (and completes authentication in the latter case).
|
||||
|
||||
The `$_username` variable set during the authentication process is then used to decide which buttons (Login or User/Logout) should be shown.
|
||||
|
||||
The `$_shell_enabled` variable controls the execution of the custom shell component. This feature is necessary because the header module is also loaded by the currencies_item_dml.sql module, which should only be accessible to authenticated users. However, the currencies_item_dml.sql module is a no-GUI module, which performs database operations and uses redirects after the requested operations are completed. At the same time, if the loaded header module executes the custom shell component, generating GUI buttons, the redirection mechanism in currencies_item_dml.sql will fail.
|
||||
|
||||
### Required variable guards
|
||||
|
||||
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);
|
||||
```
|
||||
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.
|
||||
|
||||
## Footer module - debug information
|
||||
|
||||
POST/GET/SET variables may provide helpful information for debugging purposes. In earlier [post](https://reddit.com/r/SQLpage/comments/1dh1siw/structuring_code_showing_debug_info/), I described the code I use to output variables in a convenient way. Briefly, I use `sqlpage.variables('GET')` and `sqlpage.variables('POST')` to get all variables, and I distinguish between the GET variables and local SET variables by prefixing SET variable names with an underscore. Initially, I copy-pasted the code snippets at the bottom of pages, but later I moved it to a separate file, footer_debug_post-get-set.sql, which I load via
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
```
|
||||
## Structuring code modules
|
||||
|
||||
The "currencies" table is handled by three modules:
|
||||
|
||||
- "table" view - __currencies_list.sql__
|
||||
Displays the entire table using the powerful "table" component. One way to extend this module is, possibly, to hide certain less important columns, especially for wide tables.
|
||||
- "detail" view - __currencies_item_form.sql__
|
||||
This is the "detail" view. It shows all fields for a single record. In this case, it is an "editable" form, though the fields maybe made conditionally read-only. Another possible option for a read-only detail view is to use the "datagrid" component.
|
||||
- database DML processor - __currencies_item_dml.sql__
|
||||
This is a no-GUI module, which only processes database modification operations using data submitted to the currencies_item_form.sql form. Presently, all DML statements (INSERT/UPDATE/DELETE) are processed by this module. If necessary, this module maybe split into more specialized modules.
|
||||
|
||||
Let us briefly go over the code block in these modules.
|
||||
|
||||
### Debug information (bottom section)
|
||||
|
||||
All three module load the footer module discussed above that produces a conditional output of GET/POST/SET variables.
|
||||
|
||||
### Authentication (top section)
|
||||
|
||||
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;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
This code discussed above sets the current path variable (necessary for correct redirects), authentication flag before loading the header module that takes care of authentication and common settings, such as top menu buttons.
|
||||
|
||||
The "no-GUI" currencies_item_dml.sql module does not set `$_curpath`, since it cannot be a start/end point in a redirect chain, but it sets the `$_shell_enabled` flag to suppress top menu buttons generation, as discussed earlier.
|
||||
|
||||
### Common variables
|
||||
|
||||
The second section may generally be used to set additional common variables, such as the name of the "table" view inside the "detail" view and the other way around (to switch between the two).
|
||||
|
||||
The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by the "table" view). This way, if a record is modified/deleted starting from, e.g., the "table" view, the same view is set as the final redirect target after the DML operation is completed.
|
||||
|
||||
### Table view
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### Detail view
|
||||
|
||||
The detail view module is more elaborate. If "&id" GET URL parameter is provided, the form shows the corresponding record. Otherwise, the ID field is rendered as a dropdown list populated from the database, but is set to NULL. The remaining fields are either blank or contain dummy values.
|
||||
|
||||
The first step (after the previously discussed common sections), therefore, is to filter invalid id values.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
WHERE $id = '' OR CAST($id AS INT) = 0;
|
||||
|
||||
set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided');
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
```
|
||||
|
||||
The blank string and zero are considered the equivalents of NULL, so redirect to itself is activated, removing the id parameter. If no id is provided or id is set to an integer value, the first check does not trigger. The second check above triggers when there is no record with provided id. This check resets id and displays an error message.
|
||||
|
||||
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 =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
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 = (
|
||||
WITH
|
||||
fields AS (
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
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 = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The next part defines form fields via the "dynamic" component (for some reason I am having issues with POST variables when the form is defined directly via the "form" component. Note how the $values variable prepared in previous blocks is used to populate the form. Without the SET statement, everything would need to be incorporated in a single query (which is feasible thanks to CTEs, but would still be significantly more difficult to develop and maintain).
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed.
|
||||
|
||||
The last big section defines the main form buttons, which are adjusted based on the type of operation (similarly to the form fields above).
|
||||
|
||||
The final section includes a general confirmation alert (used after INSERT/UPDATE operations) and an error alert.
|
||||
|
||||
### Coding style conventions
|
||||
|
||||
Consistent code style is important for code readability. Because SQLPage module maybe a mix of SQL code and sizeable text fragments, which may contain plain text, Markdown, JSON, HTML, etc., it might be difficult to follow a fixed set of rules. In fact, dynamically generated webpages regardless of specific technologies used tend to get messy. At the very least I strive to
|
||||
|
||||
- keep all SQL keywords always in the UPPER case,
|
||||
- have reasonably sensible code alignment (though some alignment approaches may not be generally advisable)
|
||||
- keep large static text pieces in separate appropriate dedicated files and load them via `sqlpage.read_file_as_text()` (e.g., the text of this file comes from Readme.md, where it can be properly edited by any Markdown editor and version-controlled; similarly, static JSON should go in \*.json files or in a dedicated database table with a designated JSON column).
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Redirect to the login page if the password is not correct
|
||||
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || sqlpage.url_encode($path), '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM accounts
|
||||
WHERE username = :username) AS password_hash;
|
||||
|
||||
-- The code after this point is only executed if the user has sent the correct password
|
||||
-- Generate a random session token and set via the "cookie" component in the RETURNING
|
||||
-- clause.
|
||||
|
||||
INSERT INTO sessions (id, username)
|
||||
VALUES (sqlpage.random_string(32), :username)
|
||||
RETURNING
|
||||
'cookie' AS component,
|
||||
'session_token' AS name,
|
||||
id AS value;
|
||||
|
||||
-- The user browser will now have a cookie named `session_token` that we can check later
|
||||
-- to see if the user is logged in.
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
@@ -0,0 +1,92 @@
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: var(--tblr-gray-300);
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: var(--tblr-gray-500);
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag {
|
||||
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;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #ea9999;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string {
|
||||
color: #97e1a3;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #95d1ff;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: var(--tblr-yellow);
|
||||
}
|
||||
|
||||
.token.important {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.menu_options_slim {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.menu_language_slim {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.menu_language {
|
||||
min-width: 200%;
|
||||
}
|
||||
|
||||
div.dropdown-menu- {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
a.dropdown-item- {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.slim_item {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Procesess CREATE/UPDATE/DELETE operations.
|
||||
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _session_required = 1;
|
||||
set _shell_enabled = 0;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- Redirect target must be passed as $path
|
||||
-- =============================================================================
|
||||
|
||||
set _err_msg = '&path URL GET parameter (redirect target) is not set!';
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'alert-triangle' AS icon,
|
||||
'Error' AS title,
|
||||
$_err_msg AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE
|
||||
$path IS NULL AND $DEBUG IS NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- Check new values for validity:
|
||||
-- - UPDATE existing record:
|
||||
-- :id IS NOT NULL
|
||||
-- If :name is in the database, :id must match
|
||||
-- If attempting to change :name, operation may fail due to FK constraint
|
||||
-- - INSERT new record:
|
||||
-- :id IS NULL
|
||||
-- :name is not in the database
|
||||
-- =============================================================================
|
||||
|
||||
-- Pass new values back as JSON object in $values GET variable for form population.
|
||||
--
|
||||
-- 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).
|
||||
|
||||
set _id = iif(typeof(:id) = 'text' AND :id = '', NULL, :id);
|
||||
|
||||
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');
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&op=' || $_op ||
|
||||
'&values=' || $_values ||
|
||||
'&error=' || $_err_msg AS link
|
||||
FROM currencies
|
||||
WHERE currencies.name = :name
|
||||
AND ($_id IS NULL OR currencies.id <> $_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- UPSERT: If everything is OK and "UPDATE" is indicated, update the database
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO currencies(id, name, to_rub)
|
||||
SELECT CAST($_id AS INT), :name, CAST(:to_rub AS NUMERIC)
|
||||
WHERE $action = 'UPDATE'
|
||||
ON CONFLICT(id) DO
|
||||
UPDATE SET name = excluded.name, to_rub = excluded.to_rub
|
||||
RETURNING
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&id=' || id ||
|
||||
'&info=' || $_op || ' completed successfully' AS link;
|
||||
|
||||
-- =============================================================================
|
||||
-- DELETE
|
||||
-- =============================================================================
|
||||
|
||||
DELETE FROM currencies
|
||||
WHERE $action = 'DELETE' AND id = $_id
|
||||
RETURNING
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&info=DELETE completed successfully' AS link;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,309 @@
|
||||
-- Reads an item from the database if valid id is provided and
|
||||
-- populates the form. Otherwise, an empty form is presented.
|
||||
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================== Module vars =================================
|
||||
-- =============================================================================
|
||||
|
||||
set _getpath = '?path=' || ifnull($path, $_curpath);
|
||||
set _action_target = 'currencies_item_dml.sql' || $_getpath;
|
||||
set _table_list = 'currencies_list.sql';
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== Filter invalid $id ===============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- NULL is passed as 0 or '' via POST
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
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');
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
|
||||
-- If $id IS NULL, NOT IN returns NULL and redirect is NOT selected.
|
||||
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Filter invalid $values =============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- If $values is provided, it must contain a valid JSON.
|
||||
|
||||
set _err_msg =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
|
||||
-- Covers $values IS NULL..
|
||||
|
||||
WHERE NOT json_valid($values);
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================= Prepare data ==================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- 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.
|
||||
|
||||
set _values = (
|
||||
WITH
|
||||
fields AS (
|
||||
-- If valid "id" is supplied as a GET variable, retrieve the record and
|
||||
-- populate the form.
|
||||
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
|
||||
-- If no "id" is supplied, the first part does not return any records,
|
||||
-- so add a dummy record.
|
||||
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
|
||||
-- If $value contains a valid JSON, use it to populate the form
|
||||
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================= Browse Records Button =============================
|
||||
-- =============================================================================
|
||||
--
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'square' AS shape,
|
||||
'sm' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================== Main Form ====================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- When confirming record deletion, set all fields to read-only and id type to
|
||||
-- number. No need to worry about the field values: all fields. including id are
|
||||
-- passed back as POST variables, and the code above sets the $_values variable
|
||||
-- for proper initialization of the reloaded form.
|
||||
|
||||
set _valid_ids = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'form',
|
||||
'title', 'Currency',
|
||||
'class', 'form_class',
|
||||
'id', 'detail_view',
|
||||
'validate', '',
|
||||
'action', $_action_target
|
||||
),
|
||||
json_object(
|
||||
'name', 'id',
|
||||
'label', 'ID',
|
||||
'type', iif(ifnull($action = 'DELETE', FALSE), 'number', 'select'),
|
||||
'name', 'id',
|
||||
'value', $_values ->> '$.id',
|
||||
'options', $_valid_ids,
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('false')
|
||||
),
|
||||
json_object(
|
||||
'name', 'name',
|
||||
'label', 'Currency',
|
||||
'value', $_values ->> '$.name',
|
||||
'placeholder', 'RUR',
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('true')
|
||||
),
|
||||
json_object(
|
||||
'type', 'number',
|
||||
'step', 0.01,
|
||||
'name', 'to_rub',
|
||||
'label', 'Exchange Rate to RUR',
|
||||
'value', $_values ->> '$.to_rub',
|
||||
'placeholder', 1,
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('true')
|
||||
)
|
||||
) AS properties
|
||||
;
|
||||
|
||||
-- =============================================================================
|
||||
-- ===================== Display DELETE confirmation ===========================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'warning' AS color,
|
||||
'alert-triangle' AS icon,
|
||||
TRUE AS important,
|
||||
'Warning' AS title,
|
||||
'Confirm record deletion' AS description
|
||||
WHERE $action = 'DELETE';
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== Main Form Buttons ================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- When confirming record deletion, disable the UPDATE button, replace
|
||||
-- the Reload button with the Cancel button, invert the DELETE button by
|
||||
-- removing the outline color, and ajust the POST target.
|
||||
|
||||
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'' AS size,
|
||||
'center' AS justify;
|
||||
|
||||
SELECT -- Default button
|
||||
'(Re)load' AS title,
|
||||
'read_rec' AS id,
|
||||
'database' AS icon,
|
||||
'database' AS icon_after,
|
||||
'green' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_curpath AS link,
|
||||
'detail_view' AS form,
|
||||
TRUE AS space_after
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
SELECT -- Cancel DELETE button
|
||||
'Cancel' AS title,
|
||||
'read_rec' AS id,
|
||||
'alert-triangle' AS icon,
|
||||
'alert-triangle' AS icon_after,
|
||||
'primary' AS color,
|
||||
TRUE AS narrow,
|
||||
$_curpath AS link,
|
||||
'detail_view' AS form,
|
||||
TRUE AS space_after
|
||||
WHERE ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
SELECT
|
||||
'Update' AS title, -- UPDATE / INSERT button
|
||||
'update_rec' AS id,
|
||||
'device-floppy' AS icon,
|
||||
'device-floppy' AS icon_after,
|
||||
'azure' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_action_target || '&action=UPDATE' AS link,
|
||||
'detail_view' AS form,
|
||||
ifnull($action = 'DELETE', FALSE) AS disabled,
|
||||
TRUE AS space_after;
|
||||
|
||||
SELECT -- DELETE button
|
||||
'DELETE' AS title,
|
||||
'delete_rec' AS id,
|
||||
'alert-triangle' AS icon,
|
||||
'trash' AS icon_after,
|
||||
TRUE AS narrow,
|
||||
iif(ifnull($action = 'DELETE', FALSE), NULL, 'danger') AS outline,
|
||||
iif(ifnull($action = 'DELETE', FALSE),
|
||||
$_action_target, $_curpath || '?') || '&action=DELETE' AS link,
|
||||
'danger' AS color,
|
||||
'detail_view' AS form,
|
||||
FALSE AS space_after;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display confirmation ===============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'check' AS icon,
|
||||
'Success' AS title,
|
||||
$info AS description,
|
||||
True AS dismissible
|
||||
WHERE $info IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display error message ==============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'thumb-down' AS icon,
|
||||
$op || ' error' AS title,
|
||||
$error AS description,
|
||||
True AS dismissible
|
||||
WHERE $error IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,114 @@
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================== Module vars =================================
|
||||
-- =============================================================================
|
||||
|
||||
set _getpath = '&path=' || $_curpath;
|
||||
set _item_form = 'currencies_item_form.sql';
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display confirmation ===============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'check' AS icon,
|
||||
'Success' AS title,
|
||||
$info AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE $info IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display error message ==============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'thumb-down' AS icon,
|
||||
$op || ' error' AS title,
|
||||
$error AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE $error IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== New record button ================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'New Record' AS title,
|
||||
'insert_rec' AS id,
|
||||
'circle-plus' AS icon,
|
||||
'circle-plus' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_item_form || '?' || $_getpath || '&action=INSERT' AS link
|
||||
;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================= Show the table ================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'currencies' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'Currencies' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id,
|
||||
'actions' AS markdown;
|
||||
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
to_rub,
|
||||
'[](' || $_item_form || '?' || $_getpath || '&id=' || id || ') ' ||
|
||||
'[](' || $_item_form || '?' || $_getpath || '&id=' || id || '&action=DELETE)' AS actions
|
||||
FROM currencies
|
||||
ORDER BY id;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,65 @@
|
||||
-- =============================================================================
|
||||
-- Displays GET/POST/SET variables in sorted tables for debug purposes. The
|
||||
-- variables are displayed if URL GET variable &DEBUG=1 is set OR &error is
|
||||
-- defined and not empty.
|
||||
--
|
||||
-- ## Usage
|
||||
--
|
||||
-- Execute this script at the bottom of a page script via
|
||||
--
|
||||
-- ```sql
|
||||
-- SELECT
|
||||
-- 'dynamic' AS component,
|
||||
-- sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
-- ```
|
||||
|
||||
-- GET VARIABLES --
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'GET Variables' AS contents,
|
||||
3 AS level,
|
||||
TRUE AS center
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
FALSE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'value' AS markdown
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT "key" AS variable, value
|
||||
FROM json_each(sqlpage.variables('GET'))
|
||||
WHERE $DEBUG OR $error IS NOT NULL
|
||||
ORDER BY substr("key", 1, 1) = '_', "key";
|
||||
|
||||
|
||||
-- POST VARIABLES --
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'POST Variables' AS contents,
|
||||
3 AS level,
|
||||
TRUE AS center
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
FALSE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'value' AS markdown
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT "key" AS variable, value
|
||||
FROM json_each(sqlpage.variables('POST'))
|
||||
WHERE $DEBUG OR $error IS NOT NULL
|
||||
ORDER BY "key";
|
||||
@@ -0,0 +1,175 @@
|
||||
-- =============================================================================
|
||||
-- Checks for the availablity of an active session and redirects to the login
|
||||
-- page, if necessary. Using a customized shell_ex component, shows "user" and
|
||||
-- login/logout buttons appropriately in the top-right corner.
|
||||
--
|
||||
-- Note, any additonal "shell" component settings must also be included here in
|
||||
-- the same component. It may require extending this script in a flexible way or
|
||||
-- creating a page-specific copy, which is less desirable, as it would cause code
|
||||
-- duplication.
|
||||
--
|
||||
-- ## Usage
|
||||
--
|
||||
-- Execute this script via
|
||||
--
|
||||
-- ```sql
|
||||
-- SELECT
|
||||
-- 'dynamic' AS component,
|
||||
-- sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
-- ```
|
||||
--
|
||||
-- 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;
|
||||
-- ```
|
||||
--
|
||||
-- ## Reuired SET Variables
|
||||
--
|
||||
-- $_curpath
|
||||
-- - indicates redirect target passed to the login script
|
||||
-- $_session_required
|
||||
-- - 1 - require valid active session for non-public pages
|
||||
-- - 0 - ignore active session for public pages
|
||||
-- $_shell_enabled
|
||||
-- - 1 - execute the shell component in this script (default, if not defined)
|
||||
-- - 0 - do not execute the shell component in this script
|
||||
-- Define this value to use page-specific shell component.
|
||||
-- It id also necessary for no-GUI pages, which are called via a redirect and
|
||||
-- normally redirect back after the necessary processing is completed. Such
|
||||
-- pages may still require this script to check for active session, but they
|
||||
-- will not be able to redirect back if this script outputs GUI buttons.
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================= Check required variables ============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- 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);
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================= Check active session ==============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Check if session is available.
|
||||
-- Require the user to log in again after 1 day
|
||||
|
||||
set _username = (
|
||||
SELECT username
|
||||
FROM sessions
|
||||
WHERE sqlpage.cookie('session_token') = id
|
||||
AND created_at > datetime('now', '-1 day')
|
||||
);
|
||||
|
||||
-- Redirect to the login page if the user is not logged in.
|
||||
-- Unprotected pages must
|
||||
-- set _session_required = (SELECT FALSE);
|
||||
-- before running this script
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
|
||||
-- =============================================================================
|
||||
-- ==================== Add User and Login/Logout buttons ======================
|
||||
-- =============================================================================
|
||||
--
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'CRUD with Authentication',
|
||||
'icon', 'database',
|
||||
'description', 'Description',
|
||||
'layout', 'fluid',
|
||||
|
||||
'css',
|
||||
json_array(
|
||||
'/css/prism-tabler-theme.css', -- Load for code highlighting
|
||||
'/css/style.css'
|
||||
),
|
||||
|
||||
'javascript',
|
||||
json_array(
|
||||
|
||||
-- Code highlighting scripts
|
||||
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js'
|
||||
),
|
||||
|
||||
'menu_item',
|
||||
iif($_username IS NOT NULL,
|
||||
json_array(
|
||||
json_object(
|
||||
'button', FALSE,
|
||||
'title', 'Settings',
|
||||
'icon', 'settings',
|
||||
'submenu', json_array(
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'user-circle',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'yellow',
|
||||
'outline', '',
|
||||
'link', '#',
|
||||
'tooltip', 'User profile - Not Implemented'
|
||||
),
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'logout',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'green',
|
||||
'outline', '',
|
||||
'link', '/logout.sql?path=' || $_curpath,
|
||||
'tooltip', 'Logout'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
json_array(
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'user-scan',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'warning',
|
||||
'outline', '',
|
||||
'link', '#',
|
||||
'tooltip', 'Sign Up - Not Implemented'
|
||||
),
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'login',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', '',
|
||||
'outline', 'cyan',
|
||||
'link', '/login.sql?path=' || $_curpath,
|
||||
'tooltip', 'Login'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) AS properties
|
||||
WHERE CAST($_shell_enabled AS INT) <> 0;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 122.88" style="enable-background:new 0 0 122.88 122.88" xml:space="preserve"><style type="text/css"><![CDATA[
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#A1E367;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#2394E0;}
|
||||
]]></style><g><path class="st1" d="M61.44,0c33.93,0,61.44,27.51,61.44,61.44c0,33.93-27.51,61.44-61.44,61.44C27.51,122.88,0,95.37,0,61.44 C0,27.51,27.51,0,61.44,0L61.44,0L61.44,0z"/><path class="st0" d="M11.76,93.18c-3.62-5.49-6.33-11.64-7.92-18.23l10.82,5.22l0.06,3.23c0,1.19-2.02,3.71-2.67,4.67L11.76,93.18 L11.76,93.18L11.76,93.18z M78.31,6.5c20.62,6.85,36.09,25,39.07,47.1l-1.95-0.21c-0.35,1.5-0.67,1.53-0.67,3.33c0,1.59,2,2.65,2,6 c0,0.9-2.11,2.69-2.2,3.01l-3.13-3.67v4.67l-0.47-0.02l-0.84-8.62l-1.75,0.55l-2.08-6.41l-6.85,7.16l-0.08,5.24l-2.24,1.5 l-2.38-13.44l-1.42,1.04l-3.22-4.35l-4.81,0.14l-1.84-2.11l-1.89,0.52l-3.71-4.25l-0.72,0.49l2.3,5.88h2.67v-1.33l1.33,0 c0.96,2.66,2,1.08,2,2.67c0,5.55-6.85,9.63-11.34,10.67c0.24,1,0.15,2,1.33,2c2.51,0,1.26-0.44,4-0.67 c-0.13,5.67-6.5,12.44-9.22,16.65l1.22,8.69c0.32,1.89-3.92,3.88-5.36,6.01l0.69,3.33l-1.95,0.79c-0.34,3.42-3.66,7.21-7.39,7.21 h-4c0-4.68-3.34-11.37-3.34-14.68c0-2.81,1.33-3.19,1.33-6.67c0-3.22-3.33-7.83-3.33-8.67v-5.34H45.4c-0.4-1.49-0.15-2-2-2h-0.67 c-2.91,0-2.42,1.33-5.34,1.33h-2.67c-2.41,0-6.67-7.72-6.67-8.67v-8c0-3.45,3.16-7.21,5.34-8.67v-3.33l3-3.05l1.67-0.28 c3.58,0,3.15-2,5.34-2l6,0v4.67l6.6,2.82l0.62-2.85c2.99,0.7,3.77,2.03,7.45,2.03h1.33c2.53,0,2.67-3.36,2.67-6l-5.34,0.53 l-2.33-5.06l-2.31,0.61c0.42,1.81,0.64,1.06,0.64,2.59c0,0.9-0.74,1-1.34,1.33l-2.31-5.86l-4.97-3.55l-0.66,0.65l4.23,4.45 c-0.56,1.6-0.63,6.21-2.96,2.98l2.18-1.05l-5.44-5.7l-3.26,1.27l-3.22,3.08c-0.34,2.48-1.01,3.73-3.61,3.73 c-1.73,0-0.69-0.45-3.34-0.67v-6.67h6l-1.95-4.44l-0.72,0.44v-1.34l9.75-4.49c-0.18-1.4-0.41-0.65-0.41-2.18 c0-0.09,0.65-1.32,0.67-1.34l2.52,1.57l-0.6-2.87l-3.89,0.8l-0.72-3.49c3.08-1.62,9.87-7.34,12.03-7.34l2,0 c2.11,0,7.75,2.08,8.67,3.33l-5.35-0.54l3.97,3.27l0.38-1.4l2.96-0.81l0.04-1.85h1.34v2L78.31,6.5L78.31,6.5L78.31,6.5z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-device-floppy"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
|
||||
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
category: Design
|
||||
tags: [pencil, change, update]
|
||||
version: "1.0"
|
||||
unicode: "ea98"
|
||||
-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
|
||||
<path d="M16 5l3 3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-table"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14z" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="M10 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-trash"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 7l16 0" />
|
||||
<path d="M10 11l0 6" />
|
||||
<path d="M14 11l0 6" />
|
||||
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
|
||||
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,34 @@
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'CRUD with Authentication',
|
||||
'icon', 'database',
|
||||
'description', 'Description',
|
||||
|
||||
'css',
|
||||
json_array(
|
||||
'/css/prism-tabler-theme.css'
|
||||
),
|
||||
|
||||
'javascript',
|
||||
json_array(
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js'
|
||||
)
|
||||
|
||||
)
|
||||
) AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'text' AS component,
|
||||
TRUE AS center,
|
||||
2 AS level,
|
||||
'Demo/Template CRUD with Authentication' AS title,
|
||||
sqlpage.read_file_as_text('./README.md') AS contents_md;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 0;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'text' AS component,
|
||||
TRUE AS center,
|
||||
2 AS level,
|
||||
'SQLite Introspection Information' AS title;
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Password Hash' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
-- Password Hash
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'edit' AS icon,
|
||||
'Password Hash: sqlpage.hash_password(''admin'')' AS title,
|
||||
sqlpage.hash_password('admin') AS description,
|
||||
TRUE AS dismissible;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Alert Template =================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Alert Template' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
-- ALERT
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'Alert Title' AS title,
|
||||
'Description' AS description,
|
||||
'**Bold MD**' AS description_md,
|
||||
'alert_class' AS class,
|
||||
'alert_id' AS id,
|
||||
TRUE AS dismissible,
|
||||
FALSE AS important,
|
||||
'user' AS icon,
|
||||
'https://google.com' AS link,
|
||||
'LINK TEXT' AS link_text;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ IDs ============================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'IDs' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'IDs' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT
|
||||
sqlite_version() AS "SQLite Version",
|
||||
(SELECT * FROM pragma_application_id()) AS app_id,
|
||||
(SELECT * FROM pragma_user_version()) AS user_version,
|
||||
(SELECT * FROM pragma_schema_version()) AS schema_version;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ SQLite_Master ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'SQLite_Master' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'SQLite_Master' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
TRUE AS overflow;
|
||||
|
||||
SELECT * FROM sqlite_master;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Function List ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Function List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Function List. Total ' || (SELECT count(DISTINCT name)
|
||||
FROM pragma_function_list())
|
||||
|| ' distinct' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_function_list() ORDER BY name, narg;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Collation List =================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Collation List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Collation List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_collation_list() ORDER BY rowid;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Pragma List ====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Pragma List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Pragma List. Total ' || (SELECT count(*) FROM pragma_pragma_list()) AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_pragma_list() AS functions ORDER BY name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Module List ====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Pragma List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Module List. Total ' || (SELECT count(*) FROM pragma_module_list()) AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_module_list() ORDER BY name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Table List =====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Table List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Table List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_table_list() ORDER BY type, name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Database List ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Database List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Database List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_database_list() ORDER BY seq;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||