├── .dockerignore ├── .env ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── official-site.yml │ ├── playwright.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── _default_404.sql ├── biome.json ├── build.rs ├── configuration.md ├── db-test-setup ├── mssql │ ├── Dockerfile │ ├── entrypoint.sh │ └── setup.sql └── postgres │ ├── Dockerfile │ └── ssl │ ├── README.md │ ├── privkey.pem │ ├── server.crt │ ├── server.key │ └── server.req ├── docker-compose.yml ├── docs ├── PostgreSQL_Homepage.png ├── architecture-detailed.png ├── architecture.jpg ├── architecture.png ├── cards.png ├── code-screenshots.png ├── composite-form-screenshot.png ├── cover.png ├── demo-form.png ├── demo-graph.png ├── demo-list.png ├── example-form-code-screenshot.png ├── example-form-webpage-screenshot.png ├── example-splitwise.png ├── favicon.png ├── favicon.svg ├── grocery-app.png ├── introducing-sqlpage-to-the-postgres-community.md ├── logo.png ├── logo.webp ├── screenshots école inclusive │ ├── aesh.png │ ├── hours_by_student.png │ ├── notifications.png │ ├── parameters.png │ ├── personnel.png │ ├── pupils.png │ ├── referent.png │ └── schools.png ├── sqlpage for sqlite.md ├── sqlpage high-level architecture graphviz.dot ├── sqlpage logic.drawio ├── sqlpage logic.drawio.svg ├── sqlpage.gif └── sqlpage.mp4 ├── examples ├── CRUD - Authentication │ ├── README.md │ ├── db │ │ └── Components.sqlite │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 0000_db_init.sql │ │ │ ├── 0003_auth.sql │ │ │ └── 0007_currencies.sql │ │ ├── sqlpage.json │ │ └── templates │ │ │ └── shell.handlebars │ └── www │ │ ├── README.md │ │ ├── create_session.sql │ │ ├── css │ │ ├── prism-tabler-theme.css │ │ └── style.css │ │ ├── currencies_item_dml.sql │ │ ├── currencies_item_form.sql │ │ ├── currencies_list.sql │ │ ├── footer_debug_post-get-set.sql │ │ ├── header_shell_session.sql │ │ ├── icons │ │ ├── earth-icon.svg │ │ └── outline │ │ │ ├── device-floppy.svg │ │ │ ├── edit.svg │ │ │ ├── table.svg │ │ │ └── trash.svg │ │ ├── img │ │ ├── delete_confirmation.png │ │ ├── detail_view.png │ │ ├── logout_view.png │ │ └── table_view.png │ │ ├── index.sql │ │ ├── intro.sql │ │ ├── login.sql │ │ ├── logout.sql │ │ └── menu_test │ │ ├── dummy.sql │ │ ├── dummy_menu.sql │ │ ├── menu_code.sql │ │ └── menu_demo.sql ├── PostGIS - using sqlpage with geographic data │ ├── README.md │ ├── add_point.sql │ ├── edition_form.sql │ ├── index.sql │ ├── point.sql │ ├── screenshots │ │ └── code.png │ └── sqlpage │ │ ├── migrations │ │ └── 0000_create_db.sql │ │ └── sqlpage.json ├── SQLPage developer user interface │ ├── README.md │ ├── docker-compose.yml │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 00001_table_contents.sql │ │ │ └── 0000_create_sqlpage_files_table.sql │ │ └── sqlpage.json │ └── website │ │ ├── delete.sql │ │ ├── edit.sql │ │ ├── index.sql │ │ ├── insert_file.sql │ │ ├── js │ │ └── code-editor.js │ │ └── view_table.sql ├── cards-with-remote-content │ ├── README.md │ ├── chart-example.sql │ ├── index.sql │ ├── map-example.sql │ ├── screenshot.png │ └── table-example.sql ├── charts, computations and custom components │ ├── README.md │ ├── drums by Nana Yaw Otoo.jpg │ ├── index.sql │ ├── screenshot.png │ ├── sqlpage │ │ ├── migrations │ │ │ └── 000_init.sql │ │ └── templates │ │ │ └── big_button.handlebars │ └── taptempo.sql ├── corporate-conundrum │ ├── .gitignore │ ├── Dockerfile │ ├── New Game.sql │ ├── README.md │ ├── favicon.ico │ ├── game-over.sql │ ├── game.sql │ ├── index.sql │ ├── next-question.sql │ ├── question.sql │ ├── rules.sql │ ├── sqlpage │ │ └── migrations │ │ │ ├── 00_base_structure.sql │ │ │ └── 01_questions.sql │ └── wait.sql ├── custom form component │ ├── README.md │ ├── basic.sql │ ├── docker-compose.yml │ ├── form_action.sql │ ├── index.sql │ ├── screenshot.png │ ├── shell.sql │ └── sqlpage │ │ ├── migrations │ │ └── 0001_users_and_groups.sql │ │ └── templates │ │ └── dual-list.handlebars ├── forms with a variable number of fields │ ├── README.md │ ├── index.sql │ ├── new_product_form.sql │ ├── new_product_insert.sql │ ├── order_form.sql │ ├── order_insert.sql │ ├── orders.sql │ ├── screenshots │ │ ├── home.png │ │ ├── order-form.png │ │ └── order.png │ └── sqlpage │ │ └── migrations │ │ ├── 0000_schema.sql │ │ └── 0001_initial_products.sql ├── forms-with-multiple-steps │ ├── README.md │ ├── cookies │ │ ├── finish.sql │ │ ├── index.sql │ │ ├── step_1.sql │ │ ├── step_2.sql │ │ └── step_3.sql │ ├── database │ │ ├── finish.sql │ │ ├── index.sql │ │ ├── step_1.sql │ │ ├── step_2.sql │ │ └── step_3.sql │ ├── hidden │ │ ├── finish.sql │ │ ├── illustration.png │ │ ├── index.sql │ │ ├── step_1.sql │ │ ├── step_2.sql │ │ └── step_3.sql │ ├── index.sql │ └── sqlpage │ │ └── migrations │ │ ├── 01_users.sql │ │ └── 02_database_persistence.sql ├── handle-404 │ ├── api │ │ ├── 404.sql │ │ └── index.sql │ └── index.sql ├── image gallery with user uploads │ ├── .gitignore │ ├── README.md │ ├── create_session.sql │ ├── index.sql │ ├── login.sql │ ├── logout.sql │ ├── screenshots │ │ ├── homepage.png │ │ ├── login.png │ │ └── upload.png │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 0001_images_table.sql │ │ │ └── 0002_users.sql │ │ └── sqlpage.json │ ├── upload.sql │ └── upload_form.sql ├── light-dark-toggle │ ├── README.md │ ├── biography.sql │ ├── codeconduct.sql │ ├── index.sql │ ├── presentation.sql │ ├── screenshot.png │ ├── shell.sql │ ├── sqlpage │ │ └── sqlpage.yaml │ └── toggle.sql ├── make a geographic data application using sqlite extensions │ ├── Dockerfile │ ├── README.md │ ├── add_point.sql │ ├── index.sql │ ├── point.sql │ ├── screenshots │ │ └── code.png │ └── sqlpage │ │ ├── migrations │ │ └── 0000_create_db.sql │ │ └── sqlpage.json ├── master-detail-forms │ ├── README.md │ ├── edit_user.sql │ ├── index.sql │ ├── insert_address.sql │ ├── insert_user.sql │ ├── screenshots │ │ ├── db-schema.png │ │ ├── home-screenshot.png │ │ └── user-add-screenshot.png │ └── sqlpage │ │ └── migrations │ │ └── 0000_schema.sql ├── microsoft sql server advanced forms │ ├── README.md │ ├── docker-compose.yml │ ├── index.sql │ ├── screenshots │ │ └── app.png │ ├── sqlpage │ │ └── mssql-migrations │ │ │ ├── 0001_db_init.sql │ │ │ └── README.md │ └── survey.sql ├── modeling a many to many relationship with a form │ ├── README.md │ ├── index.sql │ ├── post.sql │ ├── screenshots │ │ ├── home.png │ │ ├── post.png │ │ ├── topic.png │ │ ├── topics.png │ │ └── write.png │ ├── sqlpage │ │ └── migrations │ │ │ ├── 00_schema.sql │ │ │ ├── 01_sample_data.sql │ │ │ └── 03_sqlpage_shell.sql │ ├── topics.sql │ ├── write.sql │ └── write_submit.sql ├── multiple-choice-question │ ├── README.md │ ├── admin.sql │ ├── create_question.sql │ ├── delete_option.sql │ ├── edit_option.sql │ ├── index.sql │ ├── process.sql │ ├── results.sql │ ├── screenshots │ │ ├── admin.png │ │ ├── main_form.png │ │ └── results.png │ ├── sqlpage │ │ └── migrations │ │ │ └── 0001_create_users_table.sql │ └── website_header.json ├── mysql json handling │ ├── README.md │ ├── docker-compose.yml │ ├── index.sql │ ├── screenshots │ │ └── app.png │ ├── sqlpage │ │ └── migrations │ │ │ ├── 0001_users_and_groups.sql │ │ │ └── 0002_survey.sql │ └── survey.sql ├── nginx │ ├── README.md │ ├── docker-compose.yml │ ├── nginx │ │ └── nginx.conf │ ├── sqlpage_config │ │ ├── migrations │ │ │ └── 000_init.sql │ │ └── sqlpage.json │ └── website │ │ ├── add_comment.sql │ │ ├── index.sql │ │ └── post.sql ├── official-site │ ├── 404.sql │ ├── Dockerfile │ ├── README.md │ ├── assets │ │ ├── highlightjs-and-tabler-theme.css │ │ ├── highlightjs-launch.js │ │ ├── icon.webp │ │ └── screenshots │ │ │ ├── big_tables.webm │ │ │ ├── plot.svg │ │ │ └── user-creation-form.png │ ├── blog.sql │ ├── blog │ │ └── three-layers.svg │ ├── colors.sql │ ├── component.sql │ ├── component_not_found.sql │ ├── components.sql │ ├── custom_components.sql │ ├── documentation.sql │ ├── examples │ │ ├── authentication │ │ │ ├── basic_auth.sql │ │ │ ├── create_session_token.sql │ │ │ ├── index.sql │ │ │ ├── login.sql │ │ │ └── logout.sql │ │ ├── big_chart.sql │ │ ├── chart.sql │ │ ├── csv_download.sql │ │ ├── dynamic_shell.sql │ │ ├── from_component_options_source.sql │ │ ├── handle_csv_upload.sql │ │ ├── handle_enctype.sql │ │ ├── handle_picture_upload.sql │ │ ├── hash_password.sql │ │ ├── index.sql │ │ ├── layouts.sql │ │ ├── menu_icon.sql │ │ ├── multistep-form │ │ │ ├── index.sql │ │ │ └── result.sql │ │ ├── show_variables.sql │ │ ├── tabs.sql │ │ └── tabs │ │ │ ├── images │ │ │ ├── CRUD - Authentication.svg │ │ │ ├── PostGIS - using sqlpage with geographic data.svg │ │ │ ├── SQLPage developer user interface.svg │ │ │ ├── corporate-conundrum.svg │ │ │ ├── custom form component.svg │ │ │ ├── forms-with-multiple-steps.svg │ │ │ ├── image gallery with user uploads.svg │ │ │ ├── microsoft sql server advanced forms.svg │ │ │ ├── mysql json handling.svg │ │ │ ├── rich-text-editor.svg │ │ │ ├── roundest_pokemon_rating.svg │ │ │ ├── sending emails.svg │ │ │ ├── simple-website-example.svg │ │ │ ├── splitwise.svg │ │ │ ├── todo application (PostgreSQL).svg │ │ │ ├── user-authentication.svg │ │ │ └── web servers - apache.svg │ │ │ └── index.sql │ ├── extensions-to-sql.md │ ├── extensions-to-sql.sql │ ├── favicon.ico │ ├── functions.sql │ ├── get started.sql │ ├── get_started.sql │ ├── index-old.sql │ ├── index.sql │ ├── manual_setup.sql │ ├── performance.sql │ ├── performance.webp │ ├── pgconf │ │ ├── 2024-sqlpage-badass.pdf │ │ ├── pgconf-2023.html │ │ └── script.js │ ├── pricing.sql │ ├── robots.txt │ ├── rss.sql │ ├── safety.sql │ ├── safety.webp │ ├── search.sql │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 01_documentation.sql │ │ │ ├── 02_hero_component.sql │ │ │ ├── 03_alert_component.sql │ │ │ ├── 04_http_header.sql │ │ │ ├── 05_cookie.sql │ │ │ ├── 06_debug.sql │ │ │ ├── 07_authentication.sql │ │ │ ├── 08_functions.sql │ │ │ ├── 09_redirect.sql │ │ │ ├── 10_map.sql │ │ │ ├── 11_json.sql │ │ │ ├── 12_blog.sql │ │ │ ├── 13_tab.sql │ │ │ ├── 14_sorry_i_forked.sql │ │ │ ├── 16_timeline.sql │ │ │ ├── 18_button.sql │ │ │ ├── 20_variables_function.sql │ │ │ ├── 21_path.sql │ │ │ ├── 22_pgconf_eu.sql │ │ │ ├── 23_uploaded_file_functions.sql │ │ │ ├── 24_read_file_as_data_url.sql │ │ │ ├── 25_read_file_as_text.sql │ │ │ ├── 26_v0.17_release.sql │ │ │ ├── 27_protocol.sql │ │ │ ├── 28_tracking_component.sql │ │ │ ├── 29_divider_component.sql │ │ │ ├── 30_breadcrumb_component.sql │ │ │ ├── 31_card_docs_update.sql │ │ │ ├── 33_blog_v018.sql │ │ │ ├── 34_carousel.sql │ │ │ ├── 35_code_title.sql │ │ │ ├── 37_rss.sql │ │ │ ├── 38_run_sql.sql │ │ │ ├── 39_persist_uploaded_file.sql │ │ │ ├── 40_fetch.sql │ │ │ ├── 41_blog_performance.sql │ │ │ ├── 42_blog_video.sql │ │ │ ├── 43_request_method.sql │ │ │ ├── 44_authentication_example.sql │ │ │ ├── 45_blog_archeology.sql │ │ │ ├── 46_html.sql │ │ │ ├── 47_link.sql │ │ │ ├── 48_status_code.sql │ │ │ ├── 49_big_number.sql │ │ │ ├── 50_blog_json.sql │ │ │ ├── 51_column.sql │ │ │ ├── 52_foldable.sql │ │ │ ├── 53_blog_pgconf2024.sql │ │ │ ├── 54_blog_bompard.sql │ │ │ ├── 55_request_body.sql │ │ │ ├── 56_headers.sql │ │ │ ├── 57_client_ip.sql │ │ │ ├── 58_fetch_with_meta.sql │ │ │ ├── 59_unsafe_contents_md.sql │ │ │ ├── 60_empty_state.sql │ │ │ ├── 61_oidc_functions.sql │ │ │ ├── 62_example_cards.sql │ │ │ ├── 999_fts_search_index.sql │ │ │ └── 99_shared_id_class_attributes.sql │ │ ├── sqlpage.yaml │ │ └── templates │ │ │ └── shell-home.handlebars │ ├── sqlpage_cover_image.webp │ ├── sqlpage_illustration_alien.webp │ ├── sqlpage_illustration_components.webp │ ├── sqlpage_introduction_video.webm │ ├── sqlpage_social_preview.webp │ ├── sso │ │ ├── index.sql │ │ └── single_sign_on.md │ ├── style_pricing.css │ └── your-first-sql-website │ │ ├── custom_urls.sql │ │ ├── documentation.sql │ │ ├── final-result.png │ │ ├── first-sql-website-launch.png │ │ ├── full-website.png │ │ ├── get_started.webp │ │ ├── get_started_linux.webp │ │ ├── get_started_macos.webp │ │ ├── get_started_windows.webp │ │ ├── hello-world.png │ │ ├── hosted.sql │ │ ├── index.sql │ │ ├── migrations.md │ │ ├── migrations.sql │ │ ├── nginx.md │ │ ├── nginx.sql │ │ ├── not_found.jpg │ │ ├── sql.webp │ │ ├── tutorial-install-any.md │ │ ├── tutorial-install-macos.md │ │ ├── tutorial-install-windows.md │ │ └── tutorial.md ├── plots tables and forms │ ├── README.md │ └── index.sql ├── read-and-set-http-cookies │ ├── Dockerfile │ ├── README.md │ ├── index.sql │ ├── logout.sql │ ├── sqlpage │ │ └── sqlpage.json │ └── subdirectory │ │ └── read_cookies.sql ├── rich-text-editor │ ├── README.md │ ├── create_blog_post.sql │ ├── edit.sql │ ├── index.sql │ ├── post.sql │ ├── rich_text_editor.js │ └── sqlpage │ │ └── migrations │ │ └── 01_blog_posts.sql ├── roundest_pokemon_rating │ ├── Dockerfile │ ├── README.md │ ├── screenshots │ │ ├── results.png │ │ └── vote.png │ ├── sqlpage │ │ ├── migrations │ │ │ └── 0000_pokemon_table.sql │ │ └── templates │ │ │ ├── pokemon.handlebars │ │ │ ├── results.handlebars │ │ │ └── shell.handlebars │ └── src │ │ ├── favicon.ico │ │ ├── index.sql │ │ ├── populate.sql │ │ ├── results.sql │ │ └── vote.sql ├── sending emails │ ├── README.md │ ├── email.sql │ └── index.sql ├── simple-website-example │ ├── README.md │ ├── delete.sql │ ├── edit.sql │ ├── index.sql │ ├── sqlpage │ │ ├── migrations │ │ │ └── 0001_create_users_table.sql │ │ └── sqlpage.json │ ├── table.sql │ └── user.sql ├── single sign on │ ├── README.md │ ├── assets │ │ ├── closed.jpeg │ │ ├── homepage.md │ │ ├── homepage.png │ │ ├── keycloak_configuration.png │ │ ├── logged_in.png │ │ ├── login_page.png │ │ └── welcome.jpeg │ ├── cas │ │ ├── README.md │ │ ├── index.sql │ │ ├── login.sql │ │ ├── logout.sql │ │ └── redirect_handler.sql │ ├── docker-compose.yaml │ ├── index.sql │ ├── keycloak-configuration.json │ ├── keycloak.Dockerfile │ ├── logout.sql │ ├── protected.sql │ └── sqlpage │ │ ├── migrations │ │ └── 000_sessions.sql │ │ └── sqlpage.yaml ├── splitwise │ ├── README.md │ ├── group.sql │ ├── index.sql │ └── sqlpage │ │ └── migrations │ │ ├── 0000_schema.sql │ │ └── 0001_views.sql ├── tiny_twitter │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── index.sql │ └── sqlpage │ │ └── migrations │ │ └── 0001_tweets.sql ├── todo application (PostgreSQL) │ ├── README.md │ ├── batch.sql │ ├── delete.sql │ ├── docker-compose.yml │ ├── explanation_diagram.svg │ ├── index.sql │ ├── screenshot.png │ ├── shell.sql │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 0000_init.sql │ │ │ └── README.md │ │ └── templates │ │ │ └── README.md │ ├── timeline.sql │ └── todo_form.sql ├── todo application │ ├── README.md │ ├── delete.sql │ ├── explanation_diagram.svg │ ├── index.sql │ ├── screenshot.png │ ├── shell.sql │ ├── sqlpage │ │ ├── migrations │ │ │ ├── 0000_init.sql │ │ │ └── README.md │ │ ├── sqlpage.json │ │ └── templates │ │ │ └── README.md │ ├── timeline.sql │ └── todo_form.sql ├── user-authentication │ ├── README.md │ ├── create_user.sql │ ├── create_user_welcome_message.sql │ ├── docker-compose.yml │ ├── generate_password_hash.sql │ ├── index.sql │ ├── login.sql │ ├── logout.sql │ ├── protected_page.sql │ ├── screenshots │ │ ├── duplicate-user.png │ │ ├── homepage.png │ │ ├── secret.png │ │ ├── signin.png │ │ ├── signup-success.png │ │ └── signup.png │ ├── signin.sql │ ├── signup.sql │ └── sqlpage │ │ └── migrations │ │ ├── 0000_init.sql │ │ └── 0001_create_admin_user.sql ├── using react and other custom scripts and styles │ ├── README.md │ ├── api.sql │ ├── equations.sql │ ├── index.sql │ ├── my_react_component.js │ ├── react.sql │ ├── screenshot-css.png │ ├── screenshot-latex-math-equations.png │ ├── screenshot-math-equations.png │ ├── screenshot-react.png │ ├── sqlpage │ │ ├── migrations │ │ │ └── 0001_clicks.sql │ │ └── templates │ │ │ └── react_component.handlebars │ └── style.css └── web servers - apache │ ├── README.md │ ├── apache │ └── httpd.conf │ ├── docker-compose.yml │ ├── sqlpage_config │ └── sqlpage.json │ ├── static │ └── index.html │ └── website │ └── index.sql ├── index.sql ├── lambda.Dockerfile ├── package-lock.json ├── package.json ├── sqlpage.service ├── sqlpage ├── apexcharts.js ├── favicon.svg ├── migrations │ └── README.md ├── sqlpage.css ├── sqlpage.db ├── sqlpage.js ├── sqlpage.json ├── tabler-icons.svg ├── templates │ ├── README.md │ ├── alert.handlebars │ ├── big_number.handlebars │ ├── breadcrumb.handlebars │ ├── button.handlebars │ ├── card.handlebars │ ├── carousel.handlebars │ ├── chart.handlebars │ ├── code.handlebars │ ├── columns.handlebars │ ├── csv.handlebars │ ├── datagrid.handlebars │ ├── debug.handlebars │ ├── default.handlebars │ ├── divider.handlebars │ ├── empty_state.handlebars │ ├── error.handlebars │ ├── foldable.handlebars │ ├── form.handlebars │ ├── hero.handlebars │ ├── html.handlebars │ ├── list.handlebars │ ├── map.handlebars │ ├── rss.handlebars │ ├── shell-empty.handlebars │ ├── shell.handlebars │ ├── steps.handlebars │ ├── tab.handlebars │ ├── table.handlebars │ ├── text.handlebars │ ├── timeline.handlebars │ ├── title.handlebars │ └── tracking.handlebars └── tomselect.js ├── src ├── app_config.rs ├── dynamic_component.rs ├── file_cache.rs ├── filesystem.rs ├── lib.rs ├── main.rs ├── render.rs ├── template_helpers.rs ├── templates.rs ├── utils.rs └── webserver │ ├── content_security_policy.rs │ ├── database │ ├── connect.rs │ ├── csv_import.rs │ ├── error_highlighting.rs │ ├── execute_queries.rs │ ├── migrations.rs │ ├── mod.rs │ ├── sql.rs │ ├── sql_to_json.rs │ ├── sqlpage_functions │ │ ├── function_definition_macro.rs │ │ ├── function_traits.rs │ │ ├── functions.rs │ │ ├── http_fetch_request.rs │ │ ├── mod.rs │ │ └── url_parameter_deserializer.rs │ └── syntax_tree.rs │ ├── error_with_status.rs │ ├── http.rs │ ├── http_client.rs │ ├── http_request_info.rs │ ├── https.rs │ ├── mod.rs │ ├── oidc.rs │ ├── request_variables.rs │ ├── response_writer.rs │ ├── routing.rs │ └── static_content.rs └── tests ├── basic └── mod.rs ├── common └── mod.rs ├── components ├── any_component.sql ├── display_form_field.sql ├── display_text.sql └── mod.rs ├── core ├── .hidden.sql ├── mod.rs ├── select_temp_t.sql └── spaces in file name.sql ├── data_formats ├── csv_data.sql ├── json_columns.sql ├── json_data.sql └── mod.rs ├── end-to-end ├── .gitignore ├── official-site.spec.ts ├── package-lock.json ├── package.json └── playwright.config.ts ├── errors ├── 404.sql └── mod.rs ├── it_works.txt ├── mod.rs ├── requests ├── mod.rs ├── request_body_base64_test.sql └── request_body_test.sql ├── sql_test_files ├── README.md ├── error_arbitrary_SQL_expressions_as_function_arguments_are_not_supported.sql ├── error_cookie_component_cannot_be_used_after_data_has_already_been_sent.sql ├── error_failed_to_import_the_csv.sql ├── error_invalid_json_in_dynamic_component.sql ├── error_single_shell_per_page.sql ├── error_the_dynamic_component_requires_a_property_named.sql ├── error_too_many_nested_inclusions.sql ├── error_unknown_field.sql ├── it_works_basic_auth_password.sql ├── it_works_basic_auth_username.sql ├── it_works_case_variables.sql ├── it_works_client_ip.sql ├── it_works_coalesce_eval.sql ├── it_works_columns_component_json_nomssql_nopostgres.sql ├── it_works_concat_str_in_pseudofunction.sql ├── it_works_cookie.sql ├── it_works_create_table.sql ├── it_works_decimal.sql ├── it_works_deeply_nested_dynamic_components.sql ├── it_works_default_component.sql ├── it_works_delayed_function_call.sql ├── it_works_dynamic_nested.sql ├── it_works_dynamic_repeated_properties.sql ├── it_works_dynamic_run_sql_include.sql ├── it_works_dynamic_shell.sql ├── it_works_dynamic_toplevel.sql ├── it_works_env_var_empty.sql ├── it_works_exec.sql ├── it_works_fetch_native_json_array_impl.sql ├── it_works_fetch_post.sql ├── it_works_fetch_simple.sql ├── it_works_fetch_with_meta_error.sql ├── it_works_fetch_with_meta_simple.sql ├── it_works_hash_password.sql ├── it_works_hash_password_null.sql ├── it_works_header.sql ├── it_works_headers.sql ├── it_works_link.sql ├── it_works_lower.sql ├── it_works_markdown_in_table.sql ├── it_works_nested_sqlpage_functions.sql ├── it_works_path.sql ├── it_works_postgres_cast_syntax.sql ├── it_works_protocol.sql ├── it_works_query_string.sql ├── it_works_random_string.sql ├── it_works_read_file_as_data_url.sql ├── it_works_read_file_as_text.sql ├── it_works_request_method.sql ├── it_works_run_sql.sql ├── it_works_run_sql_from_database.sql ├── it_works_run_sql_variable_access.sql ├── it_works_set_variable.sql ├── it_works_set_variable_numeric.sql ├── it_works_set_variable_to_null.sql ├── it_works_set_variable_to_sqlpage_function.sql ├── it_works_set_variable_to_sqlpage_post_function.sql ├── it_works_shell_search.sql ├── it_works_simple.sql ├── it_works_sqlite_unicode_upper.sql ├── it_works_temp_table_accessible_in_run_sql_nomssql.sql ├── it_works_text_markdown.sql ├── it_works_text_unsafe_markdown.sql ├── it_works_uploaded_file_is_null.sql ├── it_works_url_encode.sql ├── it_works_varchar_nomysql.sql ├── it_works_variables_function.sql ├── it_works_variables_function_get_and_post.sql ├── it_works_without_a_shell.sql ├── mod.rs └── select_temp_t.sql ├── transactions ├── failed_transaction.sql ├── failed_transaction_mssql.sql ├── failed_transaction_mysql.sql └── mod.rs └── uploads ├── mod.rs ├── upload_csv_test.sql ├── upload_file_data_url_test.sql ├── upload_file_runsql_test.sql ├── upload_file_test.sql └── uploaded_file_name_test.sql /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | sqlpage.db 3 | docs/ 4 | README.md 5 | CHANGELOG.md 6 | LICENSE.txt 7 | configuration.md 8 | *Dockerfile 9 | .dockerignore 10 | .github/ 11 | examples/ 12 | mssql/ 13 | tests/ 14 | .idea/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 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 2 | COMPOSE_PROFILES=mariadb -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sql linguist-detectable=true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | What are you building with SQLPage ? 11 | 12 | (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.) 13 | 14 | What is your problem ? A description of the problem, not the solution you are proposing. 15 | 16 | What are you currently doing ? Since your solution is not implemented in SQLPage currently, what are you doing instead ? 17 | 18 | **Describe the solution you'd like** 19 | 20 | A clear and concise description of what you want to happen. 21 | 22 | **Describe alternatives you've considered** 23 | 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | 26 | **Additional context** 27 | Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | sqlpage.db 3 | .idea/ 4 | *.mm_profdata 5 | docs/presentation-pgconf.html 6 | examples/inrap_badass/ 7 | sqlpage/https/* 8 | x.sql 9 | xbed.sql 10 | **/sqlpage.bin 11 | node_modules/ 12 | sqlpage/sqlpage.db 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": ["./Cargo.toml"] 3 | } 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you find a critical security vulnerability in this repo, you can report it directly by 6 | email to the main maintainer on `contact@ophir.dev`. 7 | This will allow publishing a new safe version before the vulnerability starts being exploited. 8 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "examples/official-site/pgconf/**", 6 | "tests/end-to-end/test-results/**" 7 | ], 8 | "ignoreUnknown": true 9 | }, 10 | "formatter": { 11 | "indentStyle": "space", 12 | "indentWidth": 2 13 | }, 14 | "vcs": { 15 | "enabled": true, 16 | "useIgnoreFile": true, 17 | "clientKind": "git" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /db-test-setup/mssql/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION=2022-latest 2 | FROM mcr.microsoft.com/mssql/server:${VERSION} 3 | 4 | # Create a config directory 5 | USER root 6 | RUN mkdir -p /usr/config 7 | WORKDIR /usr/config 8 | 9 | # Bundle config source 10 | COPY entrypoint.sh /usr/config/entrypoint.sh 11 | COPY setup.sql /usr/config/setup.sql 12 | 13 | # Grant permissions for to our scripts to be executable 14 | RUN chmod +x /usr/config/entrypoint.sh 15 | RUN chown 10001 /usr/config/entrypoint.sh 16 | USER 10001 17 | 18 | ENV SA_PASSWORD="Password123!" 19 | ENV ACCEPT_EULA="Y" 20 | 21 | HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=10 \ 22 | CMD sqlcmd -S localhost -U root -P "Password123!" -Q "SELECT 1" || exit 1 23 | 24 | ENTRYPOINT ["/usr/config/entrypoint.sh"] 25 | -------------------------------------------------------------------------------- /db-test-setup/mssql/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | /opt/mssql/bin/sqlservr & 4 | pid=$! 5 | 6 | # Wait 15 seconds for SQL Server to start up 7 | sleep 15 8 | 9 | # Run the setup script to create the DB and the schema in the DB 10 | /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql -No 11 | 12 | # Wait for sqlservr to exit 13 | wait -n $pid 14 | -------------------------------------------------------------------------------- /db-test-setup/mssql/setup.sql: -------------------------------------------------------------------------------- 1 | IF DB_ID('sqlpage') IS NULL 2 | BEGIN 3 | CREATE DATABASE sqlpage; 4 | END; 5 | GO 6 | 7 | USE sqlpage; 8 | GO 9 | 10 | CREATE LOGIN root WITH PASSWORD = 'Password123!'; 11 | CREATE USER root FOR LOGIN root; 12 | GO 13 | 14 | GRANT CREATE TABLE TO root; 15 | GRANT ALTER, DELETE, INSERT, SELECT, UPDATE ON SCHEMA::dbo TO root; 16 | GO -------------------------------------------------------------------------------- /db-test-setup/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:17-alpine 2 | 3 | # Copy the SSL certificates 4 | 5 | COPY --chown=postgres:postgres ./ssl /ssl 6 | RUN chmod 700 /ssl && chmod 600 /ssl/* 7 | 8 | CMD ["postgres", "-c", "ssl=on", "-c", "ssl_cert_file=/ssl/server.crt", "-c", "ssl_key_file=/ssl/server.key"] -------------------------------------------------------------------------------- /db-test-setup/postgres/ssl/README.md: -------------------------------------------------------------------------------- 1 | # Postgres SSL setup 2 | 3 | This directory contains dummy SSL certificates for testing purposes. 4 | Don't use these certificates for anything other than testing. 5 | 6 | The goal is to test the ability of SQLPage to connect to a Postgres database using SSL. -------------------------------------------------------------------------------- /docs/PostgreSQL_Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/PostgreSQL_Homepage.png -------------------------------------------------------------------------------- /docs/architecture-detailed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/architecture-detailed.png -------------------------------------------------------------------------------- /docs/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/architecture.jpg -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/architecture.png -------------------------------------------------------------------------------- /docs/cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/cards.png -------------------------------------------------------------------------------- /docs/code-screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/code-screenshots.png -------------------------------------------------------------------------------- /docs/composite-form-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/composite-form-screenshot.png -------------------------------------------------------------------------------- /docs/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/cover.png -------------------------------------------------------------------------------- /docs/demo-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/demo-form.png -------------------------------------------------------------------------------- /docs/demo-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/demo-graph.png -------------------------------------------------------------------------------- /docs/demo-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/demo-list.png -------------------------------------------------------------------------------- /docs/example-form-code-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/example-form-code-screenshot.png -------------------------------------------------------------------------------- /docs/example-form-webpage-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/example-form-webpage-screenshot.png -------------------------------------------------------------------------------- /docs/example-splitwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/example-splitwise.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/favicon.png -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/grocery-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/grocery-app.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/logo.webp -------------------------------------------------------------------------------- /docs/screenshots école inclusive/aesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/aesh.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/hours_by_student.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/hours_by_student.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/notifications.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/parameters.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/personnel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/personnel.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/pupils.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/pupils.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/referent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/referent.png -------------------------------------------------------------------------------- /docs/screenshots école inclusive/schools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/screenshots école inclusive/schools.png -------------------------------------------------------------------------------- /docs/sqlpage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/sqlpage.gif -------------------------------------------------------------------------------- /docs/sqlpage.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/docs/sqlpage.mp4 -------------------------------------------------------------------------------- /examples/CRUD - Authentication/db/Components.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/CRUD - Authentication/db/Components.sqlite -------------------------------------------------------------------------------- /examples/CRUD - Authentication/sqlpage/migrations/0003_auth.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "accounts" ( 2 | "username" TEXT COLLATE NOCASE PRIMARY KEY, 3 | "password_hash" TEXT COLLATE BINARY NOT NULL 4 | ); 5 | 6 | CREATE TABLE IF NOT EXISTS "sessions" ( 7 | "id" TEXT COLLATE NOCASE PRIMARY KEY, 8 | "username" TEXT COLLATE NOCASE NOT NULL 9 | REFERENCES "accounts"("username"), 10 | "created_at" TEXT COLLATE NOCASE NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | 14 | -- Creates an initial user with the username `admin` and the password `admin` (hashed using sqlpage.hash_password('admin')) 15 | 16 | INSERT OR IGNORE INTO "accounts"("username", "password_hash") VALUES 17 | ('admin', '$argon2id$v=19$m=19456,t=2,p=1$4lu3hSvaqXK0dMCPZLOIPg$PUFJSB6L3J5eZ33z9WX7y0nOH6KawV2FdW0abMuPE7o'); -------------------------------------------------------------------------------- /examples/CRUD - Authentication/sqlpage/migrations/0007_currencies.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "currencies" ( 2 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, 3 | "name" TEXT COLLATE NOCASE NOT NULL UNIQUE, 4 | "to_rub" REAL NOT NULL 5 | ); 6 | 7 | INSERT OR IGNORE INTO "currencies"("name", "to_rub") VALUES 8 | ('RUR', 1), 9 | ('USD', 90), 10 | ('CNY', 12.34); -------------------------------------------------------------------------------- /examples/CRUD - Authentication/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "listen_on": "localhost:8080", 3 | "database_url": "sqlite://./db/Components.sqlite?mode=rwc", 4 | "allow_exec": false, 5 | "web_root": "./www" 6 | } 7 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/css/style.css: -------------------------------------------------------------------------------- 1 | .menu_options_slim { 2 | min-width: inherit !important; 3 | } 4 | 5 | .menu_language_slim { 6 | min-width: inherit !important; 7 | } 8 | 9 | .menu_language { 10 | min-width: 200%; 11 | } 12 | 13 | div.dropdown-menu- { 14 | min-width: inherit !important; 15 | } 16 | 17 | a.dropdown-item- { 18 | min-width: inherit !important; 19 | } 20 | 21 | .slim_item { 22 | min-width: inherit !important; 23 | } 24 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/icons/outline/device-floppy.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/icons/outline/edit.svg: -------------------------------------------------------------------------------- 1 | 7 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/icons/outline/table.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/icons/outline/trash.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/img/delete_confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/CRUD - Authentication/www/img/delete_confirmation.png -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/img/detail_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/CRUD - Authentication/www/img/detail_view.png -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/img/logout_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/CRUD - Authentication/www/img/logout_view.png -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/img/table_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/CRUD - Authentication/www/img/table_view.png -------------------------------------------------------------------------------- /examples/CRUD - Authentication/www/logout.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | 'cookie' AS component, 3 | 'session_token' AS name, 4 | TRUE AS remove; 5 | 6 | SELECT 7 | 'redirect' AS component, 8 | ifnull($path, '/login.sql') AS link -- redirect to the login page after the user logs out 9 | -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/README.md: -------------------------------------------------------------------------------- 1 | # Using PostGIS to build a geographic data application 2 | 3 | ## Introduction 4 | 5 | This is a small application that uses [PostGIS](https://postgis.net/) 6 | to save data associated with geographic coordinates. 7 | 8 | If you are using a SQLite database, see [this other example instead](../make%20a%20geographic%20data%20application%20using%20sqlite%20extensions/), 9 | which implements the same application using the `spatialite` extension for SQLite. 10 | 11 | ### Installation 12 | 13 | You need to install the `postgis` extension for postgres. Follow [the official instructions](https://postgis.net/documentation/getting_started/). 14 | 15 | Then you can instruct SQLPage to connect to your database by editing the [`sqlpage/sqlpage.json`](./sqlpage/sqlpage.json) file. 16 | 17 | ## Screenshots 18 | 19 | ![](./screenshots/code.png) -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/add_point.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO spatial_data (title, geom, description) 2 | VALUES ( 3 | :Title, 4 | ST_MakePoint( 5 | CAST(:Longitude AS REAL), 6 | CAST(:Latitude AS REAL 7 | ), 4326), 8 | :Text 9 | ) RETURNING 10 | 'redirect' AS component, 11 | 'index.sql' AS link; -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/edition_form.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' as component, 2 | 'Point edition' AS title, 3 | '/' as link, 4 | 'book' as icon; 5 | 6 | UPDATE spatial_data 7 | SET description = :Text 8 | WHERE id = $id::int AND :Text IS NOT NULL; 9 | 10 | SELECT 'form' AS component, title 11 | FROM spatial_data WHERE id = $id::int; 12 | 13 | SELECT 14 | 'Text' AS name, 15 | 'Description for the point: ' || title AS description, 16 | 'textarea' AS type, 17 | description AS value 18 | FROM spatial_data 19 | WHERE id = $id::int; 20 | 21 | SELECT 'text' as component, 22 | description as contents_md 23 | FROM spatial_data 24 | WHERE id = $id::int; -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/screenshots/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/PostGIS - using sqlpage with geographic data/screenshots/code.png -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/sqlpage/migrations/0000_create_db.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | 3 | -- Create a table with a postgis geometry column 4 | CREATE TABLE IF NOT EXISTS spatial_data ( 5 | id serial primary key NOT NULL, 6 | title text NOT NULL, 7 | geom geometry NULL, 8 | description text NOT NULL, 9 | created_at timestamp without time zone NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | 13 | CREATE OR REPLACE VIEW distances AS 14 | SELECT from_point.id AS from_id, 15 | from_point.title AS from_label, 16 | to_point.id AS to_id, 17 | to_point.title AS to_label, 18 | ST_Distance( 19 | from_point.geom, 20 | to_point.geom, 21 | TRUE 22 | ) AS distance 23 | FROM spatial_data AS from_point, spatial_data AS to_point 24 | WHERE from_point.id != to_point.id; -------------------------------------------------------------------------------- /examples/PostGIS - using sqlpage with geographic data/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "postgres://my_username:my_password@localhost:5432/my_geographic_database" 3 | } 4 | -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/README.md: -------------------------------------------------------------------------------- 1 | # Website editor 2 | 3 | SQLPage supports rendering `.sql` files that are stored directly inside the database, in a table called `sqlpage_files`. 4 | 5 | This application allows you to edit these files directly from your browser, for an easy in-browser data app authoring experience. -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version 4 | ports: 5 | - "8080:8080" 6 | volumes: 7 | - ./website:/var/www 8 | - ./sqlpage:/etc/sqlpage 9 | depends_on: 10 | - db 11 | environment: 12 | DATABASE_URL: postgres://root:secret@db/sqlpage 13 | db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases 14 | ports: 15 | - "5432:5432" 16 | - "3306:3306" 17 | image: postgres 18 | environment: 19 | POSTGRES_USER: root 20 | POSTGRES_DB: sqlpage 21 | POSTGRES_PASSWORD: secret 22 | -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/sqlpage/migrations/00001_table_contents.sql: -------------------------------------------------------------------------------- 1 | -- Given a table name as text, return the contents of the table as a set of json objects 2 | -- Safely escapes the table name to prevent SQL injection. 3 | -- Accepts only normal tables, not postgres system tables. 4 | CREATE OR REPLACE FUNCTION table_contents (table_name text) 5 | RETURNS SETOF json AS $$ 6 | BEGIN 7 | RETURN QUERY EXECUTE 8 | format('SELECT row_to_json(%I) FROM %I', table_name, table_name); 9 | END; 10 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/sqlpage/migrations/0000_create_sqlpage_files_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | sqlpage_files ( 3 | path VARCHAR(255) NOT NULL PRIMARY KEY, 4 | contents BYTEA NOT NULL, 5 | last_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 6 | ); 7 | 8 | -- automatically update last_modified timestamp 9 | 10 | CREATE OR REPLACE FUNCTION update_last_modified_sqlpage_files() 11 | RETURNS TRIGGER AS $$ 12 | BEGIN 13 | NEW.last_modified = CURRENT_TIMESTAMP; 14 | RETURN NEW; 15 | END; 16 | $$ LANGUAGE plpgsql; 17 | 18 | CREATE TRIGGER update_last_modified BEFORE 19 | UPDATE ON sqlpage_files FOR EACH ROW 20 | EXECUTE PROCEDURE update_last_modified_sqlpage_files(); -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;" 3 | } 4 | -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/website/delete.sql: -------------------------------------------------------------------------------- 1 | delete from sqlpage_files 2 | where path = $path and $confirm = 'yes' 3 | returning 4 | 'redirect' as component, 5 | sqlpage.link( 6 | 'index.sql', 7 | json_build_object('deleted', $path) 8 | ) as link; 9 | 10 | select 'alert' as component, 11 | 'Delete ' || $path || ' ?' as title, 12 | 'Are you sure you want to delete ' || $path || '?' as description, 13 | 'warning' as color, 14 | sqlpage.link('delete.sql', json_build_object('path', $path, 'confirm', 'yes')) as link, 15 | 'Delete' as link_text; -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/website/edit.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | 'js/code-editor.js' as javascript; 3 | 4 | select 5 | 'form' as component, 6 | COALESCE('Editing ' || $path, 'New page') as title, 7 | 'insert_file.sql' as action; 8 | 9 | select 10 | 'path' as name, 11 | 'text' as type, 12 | 'Name' as label, 13 | $path as value, 14 | 'test.sql' as placeholder; 15 | 16 | select 17 | 'textarea' as type, 18 | 'contents' as name, 19 | 'code-editor' as id, 20 | 'Contents' as label, 21 | (select contents from sqlpage_files where path = $path) as value, 22 | 'SELECT ''text'' as component, 23 | ''Hello, world!'' as contents;' as placeholder; -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/website/index.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'list' as component, 3 | 'Website files' as title; 4 | 5 | select 6 | path as title, 7 | path as link, 8 | sqlpage.link ('edit.sql', json_build_object ('path', path)) as edit_link, 9 | sqlpage.link ('delete.sql', json_build_object ('path', path)) as delete_link 10 | from 11 | sqlpage_files; 12 | 13 | select 14 | 'Create new file' as title, 15 | 'edit.sql' as link, 16 | 'file-plus' as icon, 17 | 'green' as color; 18 | 19 | select 'list' as component, 20 | 'Database tables' as title; 21 | 22 | select 23 | table_name as title, 24 | sqlpage.link ('view_table.sql', json_build_object('table_name', table_name)) as link 25 | from 26 | information_schema.tables 27 | where 28 | table_schema = 'public' 29 | and table_type = 'BASE TABLE'; -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/website/insert_file.sql: -------------------------------------------------------------------------------- 1 | insert into sqlpage_files (path, contents) 2 | values (:path, :contents::bytea) 3 | on conflict (path) 4 | do update set contents = excluded.contents 5 | returning 6 | 'redirect' as component, 7 | sqlpage.link( 8 | 'index.sql', 9 | json_build_object('inserted', :path) 10 | ) as link; -------------------------------------------------------------------------------- /examples/SQLPage developer user interface/website/view_table.sql: -------------------------------------------------------------------------------- 1 | select 'title' as component, $table_name as contents; 2 | select 'table' as component, true as search, true as sort; 3 | 4 | select 'dynamic' as component, t as properties 5 | from table_contents($table_name) t 6 | LIMIT 1000; 7 | 8 | select 'alert' as component, 9 | CASE 10 | WHEN COUNT(*) = 0 THEN 'The table is empty.' 11 | WHEN COUNT(*) > 1000 THEN 'Only the first 1000 rows are shown.' 12 | END as description, 13 | 'info' as color 14 | from table_contents($table_name) 15 | HAVING NOT COUNT(*) BETWEEN 1 AND 1000; -------------------------------------------------------------------------------- /examples/cards-with-remote-content/README.md: -------------------------------------------------------------------------------- 1 | # Remote Content Demo 2 | 3 | This small SQLPage example shows how to: 4 | - lazy-load other page in cards including: 5 | - chart component rendered by sqlpage 6 | - map component rendered by sqlpage 7 | - table component rendered by sqlpage 8 | 9 | ![remote content screenshot](screenshot.png) 10 | 11 | -------------------------------------------------------------------------------- /examples/cards-with-remote-content/chart-example.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'chart' as component, 3 | 'Quarterly Revenue' as title, 4 | 'area' as type, 5 | 'indigo' as color, 6 | 5 as marker, 7 | TRUE as time; 8 | select 9 | '2022-01-01T00:00:00Z' as x, 10 | 15 as y; 11 | select 12 | '2022-04-01T00:00:00Z' as x, 13 | 46 as y; 14 | select 15 | '2022-07-01T00:00:00Z' as x, 16 | 23 as y; 17 | select 18 | '2022-10-01T00:00:00Z' as x, 19 | 70 as y; 20 | select 21 | '2023-01-01T00:00:00Z' as x, 22 | 35 as y; 23 | select 24 | '2023-04-01T00:00:00Z' as x, 25 | 106 as y; 26 | select 27 | '2023-07-01T00:00:00Z' as x, 28 | 53 as y; 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/cards-with-remote-content/index.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'card' as component, 3 | 2 as columns; 4 | select 5 | 'A card with a Markdown description' as title, 6 | 'This is a card with a **Markdown** description. 7 | 8 | This is useful if you want to display a lot of text in the card, with many options for formatting, such as 9 | - **bold**, 10 | - *italics*, 11 | - [links](index.sql), 12 | - etc.' as description_md; 13 | select 14 | 'A card with lazy-loaded chart' as title, 15 | '/chart-example.sql?_sqlpage_embed' as embed; 16 | select 17 | 'A card with lazy-loaded map' as title, 18 | '/map-example.sql?_sqlpage_embed' as embed; 19 | select 20 | 'A card with lazy-loaded table' as title, 21 | '/table-example.sql?_sqlpage_embed' as embed; 22 | -------------------------------------------------------------------------------- /examples/cards-with-remote-content/map-example.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'map' as component, 3 | 1 as zoom; 4 | select 5 | 'New Delhi' as title, 6 | 28.6139 as latitude, 7 | 77.209 as longitude; 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/cards-with-remote-content/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/cards-with-remote-content/screenshot.png -------------------------------------------------------------------------------- /examples/cards-with-remote-content/table-example.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'table' as component, 3 | TRUE as sort, 4 | TRUE as search; 5 | select 6 | 'Ophir' as Forename, 7 | 'Lojkine' as Surname, 8 | 'lovasoa' as Pseudonym; 9 | select 10 | 'Linus' as Forename, 11 | 'Torvalds' as Surname, 12 | 'torvalds' as Pseudonym; 13 | 14 | -------------------------------------------------------------------------------- /examples/charts, computations and custom components/README.md: -------------------------------------------------------------------------------- 1 | # TapTempo 2 | 3 | This small SQLPage example shows how to : 4 | - save request logs to the database 5 | - display graphs 6 | - make computations on the database 7 | - write a custom component with HTML and CSS 8 | 9 | ## TapTempo: a webapp to measure the tempo of a song 10 | 11 | TapTempo is a webapp that lets you measure the tempo of a song by tapping on a button in rhythm with the music. 12 | 13 | It is a simple example of a webapp that stores data in a database, and displays graphs. 14 | 15 | ### Implementation 16 | 17 | At each tap, the webapp sends a request to the server, which stores the timestamp of the tap in the `tap` table. 18 | 19 | A [window function](https://www.sqlite.org/windowfunctions.html) is used to compute the tempo of the song from the timestamps of the last two taps. 20 | 21 | ![taptempo screenshot](screenshot.png) -------------------------------------------------------------------------------- /examples/charts, computations and custom components/drums by Nana Yaw Otoo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/charts, computations and custom components/drums by Nana Yaw Otoo.jpg -------------------------------------------------------------------------------- /examples/charts, computations and custom components/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/charts, computations and custom components/screenshot.png -------------------------------------------------------------------------------- /examples/charts, computations and custom components/sqlpage/migrations/000_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tap( 2 | tapping_session INTEGER, 3 | day REAL NOT NULL, -- fractional julian day, easy to manipulate in SQLite 4 | PRIMARY KEY (tapping_session, day) 5 | ); 6 | 7 | CREATE VIEW tap_bpm AS 8 | SELECT 9 | *, 10 | CAST( 11 | 1 / ((24 * 60) * (day - previous)) 12 | AS INTEGER 13 | ) AS bpm 14 | FROM ( 15 | SELECT 16 | *, 17 | lag(day) OVER (ORDER BY day) AS previous 18 | FROM tap 19 | ); -------------------------------------------------------------------------------- /examples/corporate-conundrum/.gitignore: -------------------------------------------------------------------------------- 1 | sqlpage.db 2 | -------------------------------------------------------------------------------- /examples/corporate-conundrum/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lovasoa/sqlpage:main 2 | 3 | COPY ./sqlpage /etc/sqlpage 4 | COPY . /var/www -------------------------------------------------------------------------------- /examples/corporate-conundrum/New Game.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO games(id) 2 | VALUES(random()) 3 | RETURNING 4 | 'redirect' as component, 5 | CONCAT('game.sql?id=', id) as link; -------------------------------------------------------------------------------- /examples/corporate-conundrum/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/corporate-conundrum/favicon.ico -------------------------------------------------------------------------------- /examples/custom form component/README.md: -------------------------------------------------------------------------------- 1 | # Custom form component 2 | 3 | This example shows how to create a simple custom component in handlebars, and call it from SQL. 4 | 5 | It uses MySQL, but it should be easy to adapt to other databases. 6 | The only MySQL-specific features used here are: 7 | - `json_table`, which is supported by MariaDB and MySQL 8.0 and later, 8 | - MySQL's `json_merge` function. 9 | 10 | Both [have analogs in other databases](https://sql-page.com/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide). 11 | 12 | ![screenshot](screenshot.png) 13 | 14 | 15 | ## Key features illustrated in this example 16 | 17 | - How to create a custom component in handlebars, with dynamic behavior implemented in JavaScript 18 | - How to manage multiple-option select boxes, with pre-selected items, and multiple choices 19 | - Including a common menu between different pages using a `shell.sql` file, the dynamic component, and the `sqlpage.run_sql` function. 20 | -------------------------------------------------------------------------------- /examples/custom form component/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version 4 | ports: 5 | - "8080:8080" 6 | volumes: 7 | - .:/var/www 8 | - ./sqlpage:/etc/sqlpage 9 | depends_on: 10 | - db 11 | environment: 12 | DATABASE_URL: mysql://root:secret@db/sqlpage 13 | db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases 14 | ports: 15 | - "3306:3306" 16 | image: mysql:9 # support for json_table was added in mariadb 10.6 17 | environment: 18 | MYSQL_ROOT_PASSWORD: secret 19 | MYSQL_DATABASE: sqlpage -------------------------------------------------------------------------------- /examples/custom form component/index.sql: -------------------------------------------------------------------------------- 1 | -- include the common menu 2 | select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; 3 | 4 | -- Call our custom component from ./sqlpage/templates/dual-list.handlebars 5 | select 6 | 'dual-list' as component, 7 | 'form_action.sql' as action; 8 | 9 | -- This SQL query returns the list of users, with a boolean indicating if they are in the group 10 | select 11 | id, 12 | name as label, 13 | group_members.group_id is not null as selected 14 | from 15 | users 16 | left join group_members on users.id = group_members.user_id 17 | and group_members.group_id = 1; -------------------------------------------------------------------------------- /examples/custom form component/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/custom form component/screenshot.png -------------------------------------------------------------------------------- /examples/custom form component/shell.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'shell' as component, 3 | 'Custom form component' as title, 4 | 'index' as menu_item, 5 | 'basic' as menu_item; -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 'Products' AS title; 2 | 3 | SELECT 'list' AS component, 'Products' AS title; 4 | SELECT 'Add a new product' AS title, 5 | 'red' AS color, 6 | 'new_product_form.sql' AS link, 7 | TRUE AS active; 8 | SELECT 'Pass an order' AS title, 9 | 'blue' AS color, 10 | 'order_form.sql' AS link, 11 | TRUE AS active; 12 | SELECT 13 | name AS title, 14 | price || '€' AS description 15 | FROM products; -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/new_product_form.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 'New product' AS title; 2 | 3 | SELECT 'form' as component, 4 | 'New product' as title, 5 | 'Create product' as validate, 6 | 'new_product_insert.sql' as action; 7 | SELECT 'Name' as name; 8 | SELECT 'Price' as name, 'number' as type; -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/new_product_insert.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO products (name, price) 2 | VALUES (:Name, :Price) 3 | RETURNING 4 | 'redirect' AS component, 5 | 'index.sql' AS link 6 | ; -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/order_form.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 'Order' AS title; 2 | 3 | SELECT 'form' as component, 4 | 'Pass an order' as title, 5 | 'Order' as validate, 6 | 'order_insert.sql' as action; 7 | 8 | SELECT 'Name' as name, 'Your full name' AS placeholder; 9 | SELECT 'Email' as name, 'Your email address' AS placeholder; 10 | SELECT id AS name, 11 | 'Quantity of ' || name || '' AS label, 12 | 'Number of ' || name || ' you wish to order, for ' || price || ' € each.' AS description, 13 | 'number' AS type, 14 | 1 AS step, 15 | 0 as min, 16 | 0 as value 17 | FROM products 18 | ORDER BY id; 19 | -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/order_insert.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO orders(customer_name, customer_email) 2 | VALUES (:Name, :Email); 3 | 4 | INSERT INTO order_items(order_id, quantity, product_id) 5 | SELECT 6 | last_insert_rowid() AS order_id, -- The id of the order we just inserted. Requires SQLPage v0.9.0 or later. 7 | CAST(value AS INTEGER) AS quantity, 8 | CAST(key AS INTEGER) AS product_id -- extracts converts the field name into a number 9 | FROM JSON_EACH(sqlpage.variables('post')) -- Iterates on all posted variables. Requires SQLPage v0.15.0 or later 10 | WHERE product_id > 0 -- we used the product id as a variable name in the product quatntity form fields 11 | and CAST(value AS INTEGER) > 0 12 | RETURNING 'redirect' as component, 'orders.sql?id=' || order_id as link; -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/forms with a variable number of fields/screenshots/home.png -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/screenshots/order-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/forms with a variable number of fields/screenshots/order-form.png -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/screenshots/order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/forms with a variable number of fields/screenshots/order.png -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/sqlpage/migrations/0000_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE products ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | name VARCHAR(255) NOT NULL, 4 | price DECIMAL(10, 2) NOT NULL 5 | ); 6 | 7 | CREATE TABLE orders ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT, 9 | customer_name VARCHAR(255) NOT NULL, 10 | customer_email VARCHAR(255) NOT NULL, 11 | date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 12 | ); 13 | 14 | CREATE TABLE order_items ( 15 | order_id INTEGER NOT NULL, 16 | product_id INTEGER NOT NULL, 17 | quantity INTEGER NOT NULL, 18 | FOREIGN KEY (order_id) REFERENCES orders (id), 19 | FOREIGN KEY (product_id) REFERENCES products (id), 20 | PRIMARY KEY (order_id, product_id) 21 | ); -------------------------------------------------------------------------------- /examples/forms with a variable number of fields/sqlpage/migrations/0001_initial_products.sql: -------------------------------------------------------------------------------- 1 | -- Insert statements to populate the 'products' table with unusual products 2 | INSERT INTO products (name, price) VALUES ('Invisible Umbrella', 99.99); 3 | INSERT INTO products (name, price) VALUES ('Silent Alarm Clock', 49.95); 4 | INSERT INTO products (name, price) VALUES ('Pet Rock 2.0', 19.99); 5 | INSERT INTO products (name, price) VALUES ('Chocolate Teapot', 39.99); 6 | INSERT INTO products (name, price) VALUES ('Square Wheels for Your Bicycle', 29.99); 7 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/cookies/finish.sql: -------------------------------------------------------------------------------- 1 | insert into users ( 2 | name, email, age 3 | ) values ( 4 | sqlpage.cookie('name'), 5 | sqlpage.cookie('email'), 6 | :age -- This is the age that was submitted from the form in step_3.sql 7 | ); 8 | 9 | -- remove cookies 10 | with t(name) as (values ('name'), ('email'), ('age')) 11 | select 'cookie' as component, name, '/cookies/' as path, true as remove from t; 12 | 13 | select 14 | 'alert' as component, 15 | 'Welcome, ' || name || '!' as title, 16 | 'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md 17 | from users where id = last_insert_rowid(); 18 | 19 | select 'list' as component, 'Existing users' as title, 'users' as value; 20 | select name as title, email as description from users; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/cookies/index.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, 'step_1.sql' as link; 2 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/cookies/step_1.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'form' as component, 3 | 'step_2.sql' as action; 4 | 5 | select 6 | 'name' as name, 7 | true as required, 8 | sqlpage.cookie ('name') as value; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/cookies/step_2.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'cookie' as component, 3 | 'name' as name, 4 | :name as value, 5 | '/cookies/' as path; -- Only send the cookie for pages in the /cookies/ directory 6 | 7 | select 8 | 'form' as component, 9 | 'step_3.sql' as action; 10 | 11 | select 12 | 'email' as name, 13 | 'email' as type, 14 | true as required, 15 | sqlpage.cookie ('email') as value, 16 | 'you@example.com' as placeholder, 17 | 'Hey ' || coalesce(:name, sqlpage.cookie('name')) || '! what is your email?' as description; 18 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/cookies/step_3.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'cookie' as component, 3 | 'email' as name, 4 | :email as value, 5 | '/cookies/' as path; 6 | 7 | select 8 | 'form' as component, 9 | 'finish.sql' as action; 10 | 11 | select 12 | 'age' as name, 13 | 'number' as type, 14 | true as required, 15 | 'How old are you, ' || sqlpage.cookie('name') || '?' as description; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/database/finish.sql: -------------------------------------------------------------------------------- 1 | update partially_filled_users set age = :age 2 | where :age is not null and id = $id; 3 | 4 | insert into users (name, email, age) 5 | select name, email, age from partially_filled_users where id = $id; 6 | 7 | delete from partially_filled_users where id = $id; 8 | 9 | select 10 | 'alert' as component, 11 | 'Welcome, ' || name || '!' as title, 12 | 'You are user #' || id || '. [Create a new user](index.sql)' as description_md 13 | from users where id = last_insert_rowid(); 14 | 15 | select 'list' as component, 'Existing users' as title, 'users' as value; 16 | select name as title, email as description from users; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/database/index.sql: -------------------------------------------------------------------------------- 1 | -- create a new empty partially_filled_users row, returning its id 2 | insert into partially_filled_users default values 3 | returning 4 | 'redirect' as component, 5 | 'step_1.sql?id=' || id as link; 6 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/database/step_1.sql: -------------------------------------------------------------------------------- 1 | select 'form' as component, 'step_2.sql?id=' || $id as action; 2 | 3 | select 'name' as name, true as required, name as value 4 | from partially_filled_users where id = $id; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/database/step_2.sql: -------------------------------------------------------------------------------- 1 | update partially_filled_users set name = :name 2 | where :name is not null and id = $id; 3 | 4 | select 'form' as component, 'step_3.sql?id=' || $id as action; 5 | 6 | select 'email' as name, 'email' as type, true as required, email as value, 7 | 'you@example.com' as placeholder, 8 | 'Hey ' || name || '! what is your email?' as description 9 | from partially_filled_users where id = $id; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/database/step_3.sql: -------------------------------------------------------------------------------- 1 | update partially_filled_users set email = :email 2 | where :email is not null and id = $id; 3 | 4 | select 'form' as component, 'finish.sql?id=' || $id as action; 5 | 6 | select 'age' as name, 'number' as type, true as required, age as value, 7 | 'How old are you, ' || name || '?' as description 8 | from partially_filled_users where id = $id; 9 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/finish.sql: -------------------------------------------------------------------------------- 1 | insert into users (name, email, age) values (:name, :email, :age) 2 | returning 3 | 'alert' as component, 4 | 'Welcome, ' || name || '!' as title, 5 | 'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md; 6 | 7 | select 'list' as component, 'Existing users' as title, 'users' as value; 8 | select name as title, email as description from users; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/forms-with-multiple-steps/hidden/illustration.png -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/index.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, 'step_1.sql' as link; 2 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/step_1.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'form' as component, 3 | 'step_2.sql' as action; 4 | 5 | select 6 | 'name' as name, 7 | true as required; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/step_2.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'form' as component, 3 | 'step_3.sql' as action; 4 | 5 | select 6 | 'email' as name, 7 | 'email' as type, 8 | true as required, 9 | 'you@example.com' as placeholder, 10 | 'Hey ' || :name || '! what is your email?' as description; 11 | 12 | with previous_answers(name, value) as (values ('name', :name)) 13 | select 'hidden' as type, name, value from previous_answers; 14 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/hidden/step_3.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'form' as component, 3 | 'finish.sql' as action; 4 | 5 | select 6 | 'age' as name, 7 | 'number' as type, 8 | true as required, 9 | 'How old are you, ' || :name || '?' as description; 10 | 11 | with previous_answers(name, value) as (values ('name', :name), ('email', :email)) 12 | select 'hidden' as type, name, value from previous_answers; 13 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/index.sql: -------------------------------------------------------------------------------- 1 | select 'list' as component, 'Forms with multiple steps' as title; 2 | 3 | select 'Database persistence' as title, 'database' as link; 4 | select 'Cookies' as title, 'cookies' as link; 5 | select 'Hidden fields' as title, 'hidden' as link; 6 | 7 | select 'text' as component, sqlpage.read_file_as_text('README.md') as contents_md; -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/sqlpage/migrations/01_users.sql: -------------------------------------------------------------------------------- 1 | -- Simple SQLite users table 2 | create table users ( 3 | id integer primary key autoincrement, 4 | name text not null, 5 | email text not null, 6 | age integer not null check(age > 0) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/forms-with-multiple-steps/sqlpage/migrations/02_database_persistence.sql: -------------------------------------------------------------------------------- 1 | -- this table will store partially filled user forms 2 | create table partially_filled_users ( 3 | id integer primary key autoincrement, 4 | name text null, -- all fields are nullable, because the user may not have filled them yet 5 | email text null, 6 | age integer null check(age > 0) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/handle-404/api/404.sql: -------------------------------------------------------------------------------- 1 | SELECT 'debug' AS component, 2 | 'api/404.sql' AS serving_file, 3 | sqlpage.path() AS request_path; 4 | 5 | SELECT 'button' AS component; 6 | SELECT 7 | 'Back home' AS title, 8 | 'home' AS icon, 9 | '/' AS link; 10 | -------------------------------------------------------------------------------- /examples/handle-404/api/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | 'title' AS component, 3 | 'Welcome to the API' AS contents; 4 | 5 | SELECT 'button' AS component; 6 | SELECT 7 | 'Back home' AS title, 8 | 'home' AS icon, 9 | '/' AS link; 10 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/.gitignore: -------------------------------------------------------------------------------- 1 | images/ -------------------------------------------------------------------------------- /examples/image gallery with user uploads/README.md: -------------------------------------------------------------------------------- 1 | # Image gallery 2 | 3 | This example shows how to create an image gallery with user uploads. 4 | 5 | ![Screenshot of the image gallery](./screenshots/homepage.png) 6 | 7 | Users can log in (default login is `admin`/`admin`) and upload images. 8 | Uploaded images are stored in the database as data urls. 9 | Using data urls allows us to store the images in the database, without having to set up a separate file server, 10 | which is the simplest way to get started. However, this is not a good idea if you want to store large images, 11 | as data urls are not very efficient, and will need to be transferred in full from the database to sqlpage to the user's browser. One can use `sqlpage.exec` to move the images to a separate file server, and store only the file urls in the database. 12 | 13 | ![Screenshot of the upload page](./screenshots/upload.png) 14 | ![Screenshot of the login page](./screenshots/login.png) -------------------------------------------------------------------------------- /examples/image gallery with user uploads/create_session.sql: -------------------------------------------------------------------------------- 1 | -- redirect to the login page if the password is not correct 2 | SELECT 'authentication' AS component, 3 | 'login.sql?error' AS link, 4 | (select password_hash from user where username = :Username) AS password_hash, 5 | :Password AS password; 6 | 7 | -- code after this line will only be executed if the user is authenticated 8 | -- (i.e. if the password that they sent matches the password hash that we have stored for them) 9 | 10 | insert into session (id, username) 11 | values (sqlpage.random_string(32), :Username) 12 | returning 13 | 'cookie' AS component, 14 | 'session_token' AS name, 15 | id AS value; 16 | 17 | -- The user browser will now have a cookie named `session_token` that we can check later 18 | -- to see if the user is logged in. 19 | select 'redirect' as component, '/' as link; 20 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/index.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | 'My image gallery' as title, 3 | ( 4 | case when sqlpage.cookie('session_token') is null then 'login' 5 | else 'logout' end 6 | ) as menu_item; 7 | 8 | select 'card' as component, 9 | 'My image gallery' as title; 10 | 11 | select title, description, image_url as top_image 12 | from image; 13 | 14 | select 'Your gallery is empty' as title, 15 | 'You have not uploaded any images yet. Click the button below to upload a new image.' as description 16 | where not exists (select 1 from image); 17 | 18 | select 'button' as component; 19 | select 20 | 'Upload a new image' as title, 21 | 'upload_form.sql' as link, 22 | 'plus' as icon, 23 | 'primary' as color; 24 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/login.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 'My image gallery' as title; 2 | 3 | select 'form' as component, 'Login' as title, 'create_session.sql' as action; 4 | select 'text' as type, 'Username' as name, true as required; 5 | select 'password' as type, 'Password' as name, true as required; 6 | 7 | 8 | select 'alert' as component, 9 | 'danger' as color, 10 | 'You are not logged in' as title, 11 | 'Sorry, we could not log you in. Please try again.' as description 12 | where $error is not null; -------------------------------------------------------------------------------- /examples/image gallery with user uploads/logout.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'cookie' AS component, 3 | 'session_token' AS name, 4 | true AS remove; 5 | 6 | select 'redirect' as component, '/login.sql' as link -- redirect to the login page after the user logs out 7 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/screenshots/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/image gallery with user uploads/screenshots/homepage.png -------------------------------------------------------------------------------- /examples/image gallery with user uploads/screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/image gallery with user uploads/screenshots/login.png -------------------------------------------------------------------------------- /examples/image gallery with user uploads/screenshots/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/image gallery with user uploads/screenshots/upload.png -------------------------------------------------------------------------------- /examples/image gallery with user uploads/sqlpage/migrations/0001_images_table.sql: -------------------------------------------------------------------------------- 1 | -- a sqlite table that will hold the image URLs together with a title and a description 2 | CREATE TABLE image ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | title TEXT NOT NULL, 5 | description TEXT NOT NULL, 6 | image_url TEXT NOT NULL, 7 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/sqlpage/migrations/0002_users.sql: -------------------------------------------------------------------------------- 1 | create table user ( 2 | username text primary key, 3 | password_hash text not null 4 | ); 5 | 6 | create table session ( 7 | id text primary key, 8 | username text not null references user(username), 9 | created_at timestamp not null default current_timestamp 10 | ); 11 | 12 | -- Creates an initial user with the username `admin` and the password `admin` (hashed using sqlpage.hash_password('admin')) 13 | insert into user (username, password_hash) 14 | values ('admin', '$argon2id$v=19$m=19456,t=2,p=1$4lu3hSvaqXK0dMCPZLOIPg$PUFJSB6L3J5eZ33z9WX7y0nOH6KawV2FdW0abMuPE7o'); -------------------------------------------------------------------------------- /examples/image gallery with user uploads/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_uploaded_file_size": 5000000 3 | } 4 | -------------------------------------------------------------------------------- /examples/image gallery with user uploads/upload_form.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, '/login.sql' as link -- redirect to the login page if the user is not logged in 2 | where not exists (select true from session where sqlpage.cookie('session_token') = id and created_at > datetime('now', '-1 day')); -- require the user to log in again after 1 day 3 | 4 | select 'form' as component, 'Upload a new image' as title, 'upload.sql' as action; 5 | select 'text' as type, 'Title' as name, true as required; 6 | select 'text' as type, 'Description' as name; 7 | select 'file' as type, 'Image' as name, 'image/*' as accept; -------------------------------------------------------------------------------- /examples/light-dark-toggle/biography.sql: -------------------------------------------------------------------------------- 1 | SELECT 'dynamic' AS component, 2 | sqlpage.run_sql('shell.sql') 3 | AS properties; 4 | 5 | SELECT 'text' AS component, 6 | 'Biography' AS title; 7 | SELECT 'Morbi fermentum porttitor bibendum. Vivamus eu tempus purus. Sed ligula risus, consectetur in ligula eu, lobortis sollicitudin tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin purus justo, lacinia in velit sed, fringilla imperdiet neque. Suspendisse iaculis lacus metus, at imperdiet justo rutrum nec. Duis accumsan fermentum nisi quis ornare. Aenean at placerat quam, quis gravida diam. Sed sollicitudin justo sit amet mattis eleifend. Vestibulum eget porttitor quam.' AS contents; 8 | -------------------------------------------------------------------------------- /examples/light-dark-toggle/codeconduct.sql: -------------------------------------------------------------------------------- 1 | SELECT 'dynamic' AS component, 2 | sqlpage.run_sql('shell.sql') 3 | AS properties; 4 | 5 | SELECT 'text' AS component, 6 | 'Code Of Conduct' AS title; 7 | SELECT 'Pellentesque sed consequat ligula. Ut fermentum elit diam, sit amet ullamcorper orci volutpat quis. Nunc nec ipsum eu nibh interdum interdum ut vitae neque. Sed ac hendrerit tortor, ac tincidunt nibh. Mauris vel tempor odio, quis varius lorem. In sed nibh placerat, fermentum nisl eget, dictum orci. Nullam sit amet ligula velit. Maecenas faucibus massa a orci pharetra, eu fringilla enim ornare. Vestibulum quis rutrum nisi. Pellentesque nec nulla eu tellus aliquet bibendum accumsan egestas dui. Phasellus arcu felis, dictum venenatis metus vel, consectetur finibus enim. Praesent tristique semper dolor, a mollis orci pharetra vel. Vivamus mattis, lectus blandit finibus euismod, magna justo ornare nisi, vel convallis nisi velit eu purus. Aliquam erat volutpat.' AS contents; 8 | -------------------------------------------------------------------------------- /examples/light-dark-toggle/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/light-dark-toggle/screenshot.png -------------------------------------------------------------------------------- /examples/light-dark-toggle/shell.sql: -------------------------------------------------------------------------------- 1 | -- This shell goes to every page 2 | 3 | SELECT 'shell' AS component, 4 | 'LightDark' AS title, 5 | sqlpage.cookie('lightdarkstatus') AS theme, 6 | '/' AS link, 7 | '[ 8 | {"title":"Categories", 9 | "submenu": [ 10 | {"title":"Home","link":"/"}, 11 | {"title":"Presentation","link":"/presentation.sql"}, 12 | {"title":"Biography","link":"/biography.sql"}, 13 | {"title":"Code of conduct","link":"/codeconduct.sql"} 14 | ]}, 15 | {"title":"☀","link":"/toggle.sql"} 16 | ]' AS menu_item, 17 | 'sqlpage ' || sqlpage.version() 18 | AS footer; 19 | -------------------------------------------------------------------------------- /examples/light-dark-toggle/sqlpage/sqlpage.yaml: -------------------------------------------------------------------------------- 1 | database_url: "sqlite://:memory:" 2 | port: 5005 3 | -------------------------------------------------------------------------------- /examples/light-dark-toggle/toggle.sql: -------------------------------------------------------------------------------- 1 | SELECT 'cookie' AS component, 2 | 'lightdarkstatus' AS name, 3 | IIF(COALESCE(sqlpage.cookie('lightdarkstatus'),'') = '', 'dark', '') AS value; 4 | 5 | SELECT 'redirect' AS component, sqlpage.header('referer') AS link; 6 | -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | 3 | COPY --from=lovasoa/sqlpage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage 4 | 5 | RUN apt-get update && \ 6 | apt-get -y install libsqlite3-mod-spatialite 7 | 8 | COPY . . 9 | 10 | CMD ["sqlpage"] 11 | -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/README.md: -------------------------------------------------------------------------------- 1 | # Using spatialite to build a geographic data application 2 | 3 | ## Introduction 4 | 5 | This is a small application that uses [spatialite](https://www.gaia-gis.it/fossil/libspatialite/index) 6 | to save data associated to greographic coordinates. 7 | 8 | If you are using a postgres database, see [this other example instead](../PostGIS%20-%20using%20sqlpage%20with%20geographic%20data/), 9 | which implements the same application using the `PostGIS` extension. 10 | 11 | ### Installation 12 | 13 | You need to install the `spatialite` extension for SQLite. On Debian, or Ubuntu, you can do it with: 14 | 15 | ```bash 16 | sudo apt install libsqlite3-mod-spatialite 17 | ``` 18 | 19 | Then you can run this application normally with SQLPage. 20 | 21 | Notice the `sqlite_extensions` configuration parameter in [`sqlpage/sqlpage.json`](./sqlpage/sqlpage.json). 22 | 23 | ## Screenshots 24 | 25 | ![](./screenshots/code.png) -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/add_point.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO spatial_data (title, geom, description) 2 | VALUES ( 3 | :Title, 4 | MakePoint( 5 | CAST(:Longitude AS REAL), 6 | CAST(:Latitude AS REAL 7 | ), 4326), 8 | :Text 9 | ) RETURNING 10 | 'redirect' AS component, 11 | 'index.sql' AS link; -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/screenshots/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/make a geographic data application using sqlite extensions/screenshots/code.png -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/sqlpage/migrations/0000_create_db.sql: -------------------------------------------------------------------------------- 1 | -- Create a spatialite-enabled database 2 | CREATE TABLE spatial_data ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | title TEXT NOT NULL, 5 | geom POINT, 6 | description TEXT NOT NULL, 7 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 8 | ); 9 | 10 | SELECT InitSpatialMetaData(); 11 | 12 | CREATE VIEW distances AS 13 | SELECT from_point.id AS from_id, 14 | from_point.title AS from_label, 15 | to_point.id AS to_id, 16 | to_point.title AS to_label, 17 | ST_Distance( 18 | from_point.geom, 19 | to_point.geom, 20 | TRUE 21 | ) AS distance 22 | FROM spatial_data AS from_point, spatial_data AS to_point 23 | WHERE from_point.id != to_point.id; -------------------------------------------------------------------------------- /examples/make a geographic data application using sqlite extensions/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqlite_extensions": ["mod_spatialite"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/master-detail-forms/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 'hero' as component, 2 | 'SQLPage Form Demo' as title, 3 | 'This application allows you to manage a list of users and their addresses' as description_md, 4 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Lac_de_Zoug.jpg/640px-Lac_de_Zoug.jpg' as image, 5 | 'edit_user.sql' as link, 6 | 'Create a new user' as link_text; 7 | 8 | SELECT 'list' AS component, 'Users' AS title; 9 | SELECT first_name || ' ' || last_name AS title, email AS description, 'edit_user.sql?id=' || id AS link FROM "user"; 10 | SELECT 'Add a new user' AS title, 'edit_user.sql' AS link, TRUE AS active; -------------------------------------------------------------------------------- /examples/master-detail-forms/insert_address.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO address (user_id, street, city, country) VALUES ($user_id, :Street, :City, :Country) 2 | RETURNING 3 | 'redirect' AS component, 4 | 'edit_user.sql?id=' || user_id AS link; -------------------------------------------------------------------------------- /examples/master-detail-forms/insert_user.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user (id, first_name, last_name, email) VALUES (CAST($id AS INT), :"First name", :"Last name", :Email) 2 | ON CONFLICT (id) DO -- this syntax is PostgreSQL-specific. In SQLite, use ON CONFLICT IGNORE. 3 | UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, email = excluded.email 4 | RETURNING 5 | 'redirect' AS component, 6 | 'edit_user.sql?id=' || id AS link; -------------------------------------------------------------------------------- /examples/master-detail-forms/screenshots/db-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/master-detail-forms/screenshots/db-schema.png -------------------------------------------------------------------------------- /examples/master-detail-forms/screenshots/home-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/master-detail-forms/screenshots/home-screenshot.png -------------------------------------------------------------------------------- /examples/master-detail-forms/screenshots/user-add-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/master-detail-forms/screenshots/user-add-screenshot.png -------------------------------------------------------------------------------- /examples/master-detail-forms/sqlpage/migrations/0000_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "user"( 2 | id INTEGER PRIMARY KEY, 3 | first_name TEXT NOT NULL, 4 | last_name TEXT NOT NULL, 5 | email TEXT 6 | ); 7 | 8 | CREATE TABLE "address"( 9 | id INTEGER PRIMARY KEY, 10 | user_id INTEGER NOT NULL REFERENCES "user"(id), 11 | street TEXT NOT NULL, 12 | city TEXT NOT NULL, 13 | country TEXT NOT NULL 14 | ); -------------------------------------------------------------------------------- /examples/microsoft sql server advanced forms/screenshots/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/microsoft sql server advanced forms/screenshots/app.png -------------------------------------------------------------------------------- /examples/microsoft sql server advanced forms/sqlpage/mssql-migrations/README.md: -------------------------------------------------------------------------------- 1 | # Migrations for Microsoft SQL Server 2 | 3 | This folder contains the migrations for the Microsoft SQL Server example. 4 | 5 | At the time of writing, SQLPage does not support applying migrations for Microsoft SQL Server 6 | automatically, so we need to apply them manually. 7 | 8 | We write the migrations in a folder called `mssql-migrations`, instead of the usual `migrations` 9 | folder, and we use the `sqlcmd` tool to apply them. 10 | 11 | See [how it is done in the docker-compose file](../../docker-compose.yml). 12 | -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/README.md: -------------------------------------------------------------------------------- 1 | # A simple blog with topics and posts 2 | 3 | Users can create topics and posts. A topic can have many posts and a post can have many topics. 4 | 5 | See 6 | - [`write.sql`](./write.sql) for the form definition, 7 | - [`write_submit.sql`](./write_submit.sql) for the database insertion code. 8 | 9 | ![](./screenshots/home.png) 10 | ![](./screenshots/topics.png) 11 | ![](./screenshots/topic.png) 12 | ![](./screenshots/post.png) 13 | ![](./screenshots/write.png) -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/post.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM sqlpage_shell 3 | LIMIT 1; 4 | 5 | SELECT 'text' AS component; 6 | 7 | SELECT content || ' 8 | 9 | --- 10 | 11 | Published on: _' || created_at || '_ in category [' || topic.name || '](/?topic=' || topic.id || ')' || '. 12 | 13 | Other associated categories: ' || ( 14 | SELECT group_concat('[' || name || '](/?topic=' || topic_id || ')', ', ') 15 | FROM topic_post 16 | INNER JOIN topic ON topic.id = topic_post.topic_id 17 | WHERE post_id = $id 18 | ) || '.' AS contents_md 19 | FROM post 20 | INNER JOIN topic ON topic.id = post.main_topic_id 21 | WHERE post.id = $id; -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/modeling a many to many relationship with a form/screenshots/home.png -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/screenshots/post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/modeling a many to many relationship with a form/screenshots/post.png -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/screenshots/topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/modeling a many to many relationship with a form/screenshots/topic.png -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/screenshots/topics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/modeling a many to many relationship with a form/screenshots/topics.png -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/screenshots/write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/modeling a many to many relationship with a form/screenshots/write.png -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/sqlpage/migrations/03_sqlpage_shell.sql: -------------------------------------------------------------------------------- 1 | -- Instead of adding a long select at the top of all pages. 2 | -- We want to be able to to a simple query like: 3 | -- SELECT * FROM shell LIMIT 1; 4 | 5 | CREATE TABLE sqlpage_shell ( 6 | component TEXT NOT NULL, 7 | title TEXT NOT NULL, 8 | link TEXT NOT NULL, 9 | menu_item TEXT NOT NULL, 10 | lang TEXT NOT NULL, 11 | description TEXT NOT NULL, 12 | font TEXT NOT NULL, 13 | font_size INTEGER NOT NULL, 14 | icon TEXT NOT NULL, 15 | footer TEXT NOT NULL 16 | ); 17 | 18 | INSERT INTO sqlpage_shell ( 19 | component, title, link, menu_item, lang, description, font, font_size, icon, footer 20 | ) VALUES ( 21 | 'shell', 'SQL Blog', '/', 'topics', 'en-US', 'A cool SQL-only blog', 'Playfair Display', 21, 'book', 'This blog is written entirely in SQL with [SQLPage](https://sql-page.com)' 22 | ); -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/topics.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM sqlpage_shell LIMIT 1; 2 | 3 | SELECT 'list' as component, 4 | 'Topics' as title; 5 | SELECT name as title, 6 | '/?topic=' || id as link, 7 | nb_posts || ' posts. '|| 8 | COALESCE('Last post on *' || last_post || '*', '') as description_md, 9 | CASE 10 | WHEN last_post > date('now', '-2 days') THEN 'red' 11 | ELSE NULL 12 | END as color, 13 | icon, 14 | last_post > date('now', '-2 days') as active 15 | FROM topic_with_stats; -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/write.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM sqlpage_shell LIMIT 1; 2 | 3 | SELECT 'form' AS component, 'Publish !' AS validate, 'write_submit.sql' AS action; 4 | SELECT 'Title' AS name, 'text' AS type, 'Title of the post. Write something that makes people want to click !' AS description, TRUE AS required; 5 | SELECT 'Content' AS name, 'textarea' AS type, 'The content of the post. Write something exciting !' AS description, TRUE AS required; 6 | SELECT 'Main Topic' AS name, 7 | 'select' AS type, 8 | 'The main topic of the post. This will be used to display the post in the main page.' AS description, 9 | json_group_array(json_object('label', name, 'value', id)) AS options 10 | FROM topic; 11 | SELECT 'Topics[]' AS name, 'checkbox' AS type, 'Check if this post should also appear in the "' || topic.name || '" category.' AS description, topic.id AS value, topic.name AS label FROM topic; -------------------------------------------------------------------------------- /examples/modeling a many to many relationship with a form/write_submit.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO post (title, content, main_topic_id) 2 | VALUES (:Title, :Content, :"Main Topic"); 3 | 4 | INSERT INTO topic_post (topic_id, post_id) 5 | SELECT CAST(topic.value AS INTEGER), 6 | last_insert_rowid() 7 | FROM json_each(:Topics) AS topic -- we receive the value from checkboxes as a JSON array 8 | WHERE topic.value IS NOT NULL; 9 | 10 | SELECT 'redirect' AS component, 'post.sql?id=' || last_insert_rowid() AS link; -------------------------------------------------------------------------------- /examples/multiple-choice-question/create_question.sql: -------------------------------------------------------------------------------- 1 | insert into dog_lover_profiles(profile_description, score) values ('', 50) 2 | returning 3 | 'redirect' as component, 4 | 'admin.sql' as link; -------------------------------------------------------------------------------- /examples/multiple-choice-question/delete_option.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, 'admin.sql?cannot_delete' as link 2 | where exists (select 1 from answers where profile_id = $id); 3 | 4 | delete from dog_lover_profiles where id = $id 5 | returning 6 | 'redirect' as component, 7 | 'admin.sql?deleted' as link; -------------------------------------------------------------------------------- /examples/multiple-choice-question/edit_option.sql: -------------------------------------------------------------------------------- 1 | update dog_lover_profiles 2 | set profile_description = :profile_description, score = :score 3 | where id = $id 4 | returning 5 | 'redirect' as component, 6 | 'admin.sql?saved' as link; -------------------------------------------------------------------------------- /examples/multiple-choice-question/index.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.read_file_as_text('website_header.json') as properties; 2 | 3 | SELECT 4 | 'form' AS component, 5 | 'What dog lover are you ?' AS title, 6 | 'process.sql' AS action; 7 | 8 | select 'radio' as type, 'profile' as name, id as value, profile_description as label 9 | from dog_lover_profiles; -------------------------------------------------------------------------------- /examples/multiple-choice-question/process.sql: -------------------------------------------------------------------------------- 1 | insert into answers(profile_id) 2 | select CAST(:profile as integer) 3 | where :profile is not null 4 | returning 'redirect' as component, 'results.sql' as link; -------------------------------------------------------------------------------- /examples/multiple-choice-question/results.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.read_file_as_text('website_header.json') as properties; 2 | 3 | select timestamp, profile_description, score from answers 4 | inner join dog_lover_profiles on dog_lover_profiles.id = answers.profile_id; 5 | 6 | select 'csv' as component; 7 | select * from answers; -------------------------------------------------------------------------------- /examples/multiple-choice-question/screenshots/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/multiple-choice-question/screenshots/admin.png -------------------------------------------------------------------------------- /examples/multiple-choice-question/screenshots/main_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/multiple-choice-question/screenshots/main_form.png -------------------------------------------------------------------------------- /examples/multiple-choice-question/screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/multiple-choice-question/screenshots/results.png -------------------------------------------------------------------------------- /examples/multiple-choice-question/sqlpage/migrations/0001_create_users_table.sql: -------------------------------------------------------------------------------- 1 | create table dog_lover_profiles( 2 | id integer primary key, 3 | profile_description text not null, 4 | score integer not null 5 | ); 6 | 7 | insert into dog_lover_profiles(profile_description, score) 8 | values ('I love dogs', 100), ('I hate them', 0); 9 | 10 | create table answers( 11 | id integer primary key, 12 | profile_id integer not null references dog_lover_profiles(id), 13 | timestamp timestamp not null default current_timestamp 14 | ); -------------------------------------------------------------------------------- /examples/multiple-choice-question/website_header.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": "shell", 3 | "title": "SQLPage Questions", 4 | "icon": "help-hexagon", 5 | "link": "/index.sql", 6 | "menu_item": ["index", "results", "admin"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/mysql json handling/README.md: -------------------------------------------------------------------------------- 1 | # Handling json data in MySQL 2 | 3 | This demonstrates both how to produce json data from a SQL query in MySQL 4 | and how to consume json data from SQLPage. 5 | 6 | ![](./screenshots/app.png) 7 | 8 | ## Documentation 9 | 10 | This example demonstrates how to consume [JSON](https://en.wikipedia.org/wiki/JSON) data from a MySQL database, 11 | using the [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html) 12 | function to parse the JSON data into a table. 13 | 14 | The main page contains a form with a select input, in which the user can 15 | select one or multiple values. 16 | 17 | The values are then sent to the server, and are accessible from SQL queries in the form of a JSON array. 18 | 19 | The SQL then uses the `JSON_TABLE` function to transform the JSON array into a temporary table, 20 | which is then used to process the data and insert it into the database. -------------------------------------------------------------------------------- /examples/mysql json handling/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version 4 | ports: 5 | - "8080:8080" 6 | volumes: 7 | - .:/var/www 8 | - ./sqlpage:/etc/sqlpage 9 | depends_on: 10 | - db 11 | environment: 12 | DATABASE_URL: mysql://root:secret@db/sqlpage 13 | db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases 14 | ports: 15 | - "3306:3306" 16 | image: mariadb:10.6 # support for json_table was added in mariadb 10.6 17 | environment: 18 | MYSQL_ROOT_PASSWORD: secret 19 | MYSQL_DATABASE: sqlpage -------------------------------------------------------------------------------- /examples/mysql json handling/screenshots/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/mysql json handling/screenshots/app.png -------------------------------------------------------------------------------- /examples/mysql json handling/sqlpage/migrations/0001_users_and_groups.sql: -------------------------------------------------------------------------------- 1 | create table users ( 2 | id int primary key auto_increment, 3 | name varchar(255) not null 4 | ); 5 | 6 | create table groups ( 7 | id int primary key auto_increment, 8 | name varchar(255) not null 9 | ); 10 | 11 | create table group_members ( 12 | group_id int not null, 13 | user_id int not null, 14 | primary key (group_id, user_id), 15 | foreign key (group_id) references groups (id), 16 | foreign key (user_id) references users (id) 17 | ); -------------------------------------------------------------------------------- /examples/mysql json handling/sqlpage/migrations/0002_survey.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE questions( 2 | id INT PRIMARY KEY AUTO_INCREMENT, 3 | question_text TEXT 4 | ); 5 | 6 | CREATE TABLE survey_answers( 7 | id INT PRIMARY KEY AUTO_INCREMENT, 8 | question_id INT, 9 | answer TEXT, 10 | timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 11 | FOREIGN KEY (question_id) REFERENCES questions(id) 12 | ); 13 | 14 | 15 | INSERT INTO questions(question_text) VALUES 16 | ('What is your name?'), 17 | ('What is your age?'), 18 | ('What is your favorite color?'); 19 | -------------------------------------------------------------------------------- /examples/mysql json handling/survey.sql: -------------------------------------------------------------------------------- 1 | SELECT 'form' as component, 'Survey' as title; 2 | SELECT id as name, question_text as label, 'textarea' as type 3 | FROM questions; 4 | 5 | -- Save all the answers to the database, whatever the number and id of the questions 6 | INSERT INTO survey_answers (question_id, answer) 7 | SELECT 8 | question_id, 9 | json_unquote( 10 | json_extract( 11 | sqlpage.variables('post'), 12 | concat('$."', question_id, '"') 13 | ) 14 | ) 15 | FROM json_table( 16 | json_keys(sqlpage.variables('post')), 17 | '$[*]' columns (question_id int path '$') 18 | ) as question_ids; 19 | 20 | -- Show the answers 21 | select 'card' as component, 'Survey results' as title; 22 | select 23 | questions.question_text as title, 24 | survey_answers.answer as description, 25 | 'On ' || survey_answers.timestamp as footer 26 | from survey_answers 27 | inner join questions on questions.id = survey_answers.question_id; 28 | -------------------------------------------------------------------------------- /examples/nginx/sqlpage_config/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_database_pool_connections": 10, 3 | "database_connection_idle_timeout_seconds": 1800, 4 | "max_uploaded_file_size": 10485760, 5 | "compress_responses": false, 6 | "environment": "production" 7 | } 8 | -------------------------------------------------------------------------------- /examples/nginx/website/add_comment.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO comments (post_id, user_id, content) VALUES ($id, 1, :content); 2 | SELECT 'redirect' as component, '/post/' || $id AS link; -------------------------------------------------------------------------------- /examples/nginx/website/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 'list' AS component, 'Blog Posts' AS title; 2 | 3 | SELECT 4 | p.title, 5 | u.username AS description, 6 | 'user' AS icon, 7 | '/post/' || p.id AS link 8 | FROM posts p 9 | JOIN users u ON p.user_id = u.id 10 | ORDER BY p.created_at DESC; -------------------------------------------------------------------------------- /examples/official-site/404.sql: -------------------------------------------------------------------------------- 1 | select 'status_code' as component, 404 as status; 2 | select 'http_header' as component, 'no-store, max-age=0' as "Cache-Control"; 3 | select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; 4 | 5 | select 'hero' as component, 6 | 'Not Found' as title, 7 | 'Sorry, we couldn''t find the page you were looking for.' as description_md, 8 | '/your-first-sql-website/not_found.jpg' as image; 9 | -------------------------------------------------------------------------------- /examples/official-site/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lovasoa/sqlpage:main 2 | 3 | COPY ./sqlpage /etc/sqlpage 4 | COPY . /var/www -------------------------------------------------------------------------------- /examples/official-site/README.md: -------------------------------------------------------------------------------- 1 | # Official SQLPage site 2 | 3 | The SQLPage website is of course built with SQLPage ! 4 | 5 | Things that you may be interested to look at: 6 | - The [custom component we use for our stylish home page](./sqlpage/templates/shell-home.handlebars) 7 | - The [migrations](./sqlpage/migrations) that populate the database with the documentation for all components 8 | - The [advanced multistep form example](./examples/multistep-form/) 9 | 10 | It is hosted as a simple Docker container on [sql-page.com](https://sql-page.com). 11 | 12 | Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything ! -------------------------------------------------------------------------------- /examples/official-site/assets/highlightjs-launch.js: -------------------------------------------------------------------------------- 1 | hljs.highlightAll(); 2 | -------------------------------------------------------------------------------- /examples/official-site/assets/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/assets/icon.webp -------------------------------------------------------------------------------- /examples/official-site/assets/screenshots/big_tables.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/assets/screenshots/big_tables.webm -------------------------------------------------------------------------------- /examples/official-site/assets/screenshots/user-creation-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/assets/screenshots/user-creation-form.png -------------------------------------------------------------------------------- /examples/official-site/blog.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, '/blog.sql' as link 2 | where ($post IS NULL AND sqlpage.path() <> '/blog.sql') OR ($post IS NOT NULL AND NOT EXISTS (SELECT 1 FROM blog_posts WHERE title = $post)); 3 | 4 | select 'dynamic' as component, json_patch(json_extract(properties, '$[0]'), json_object( 5 | 'title', coalesce($post || ' - ', '') || 'SQLPage Blog' 6 | )) as properties 7 | FROM example WHERE component = 'shell' LIMIT 1; 8 | 9 | SELECT 'text' AS component, 10 | true as article, 11 | content AS contents_md 12 | FROM blog_posts 13 | WHERE title = $post; 14 | 15 | SELECT 'list' AS component, 16 | 'SQLPage blog' AS title; 17 | SELECT title, 18 | description, 19 | icon, 20 | sqlpage.link( 21 | COALESCE(external_url, ''), 22 | CASE WHEN external_url IS NULL THEN json_object('post', title) ELSE NULL END 23 | ) AS link 24 | FROM blog_posts 25 | ORDER BY created_at DESC; -------------------------------------------------------------------------------- /examples/official-site/components.sql: -------------------------------------------------------------------------------- 1 | SELECT 'redirect' as component, 'documentation.sql' as link; 2 | -------------------------------------------------------------------------------- /examples/official-site/examples/authentication/create_session_token.sql: -------------------------------------------------------------------------------- 1 | -- delete expired sessions 2 | delete from user_sessions where created_at < datetime('now', '-1 day'); 3 | 4 | -- check that the 5 | SELECT 'authentication' AS component, 6 | 'login.sql?failed' AS link, -- redirect to the login page on error 7 | (SELECT password_hash FROM users WHERE username = :Username) AS password_hash, -- this is a hash of the password 'admin' 8 | :Password AS password; -- this is the password that the user sent through our form in 'index.sql' 9 | 10 | -- if we haven't been redirected, then the password is correct 11 | -- create a new session 12 | insert into user_sessions (session_token, username) values (sqlpage.random_string(32), :Username) 13 | returning 'cookie' as component, 'session_token' as name, session_token as value; 14 | 15 | -- redirect to the authentication example home page 16 | select 'redirect' as component, '/examples/authentication' as link; -------------------------------------------------------------------------------- /examples/official-site/examples/authentication/index.sql: -------------------------------------------------------------------------------- 1 | -- redirect the user to the login page if they are not logged in 2 | -- this query should be present at the top of every page that requires authentication 3 | set user_role = (select role from users natural join user_sessions where session_token = sqlpage.cookie('session_token')); 4 | select 'redirect' as component, 'login.sql' as link where $user_role is null; 5 | 6 | select 'dynamic' as component, 7 | json_insert(properties, '$[0].menu_item[#]', 'logout') as properties 8 | FROM example WHERE component = 'shell' LIMIT 1; 9 | 10 | select 'alert' as component, 'info' as color, CONCAT('You are logged in as ', $user_role) as title; 11 | 12 | select 'text' as component, ' 13 | # Authentication 14 | 15 | Read the [source code](//github.com/sqlpage/SQLPage/blob/main/examples/official-site/examples/authentication/) for this demo. 16 | 17 | [Log out](logout.sql) 18 | ' as contents_md; -------------------------------------------------------------------------------- /examples/official-site/examples/authentication/logout.sql: -------------------------------------------------------------------------------- 1 | delete from user_sessions 2 | where session_token = sqlpage.cookie('session_token'); 3 | 4 | select 'redirect' as component, 'login.sql' as link; -------------------------------------------------------------------------------- /examples/official-site/examples/big_chart.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'chart' as component, 3 | 'bar' as type, 4 | TRUE as toolbar, 5 | TRUE as time; 6 | 7 | with recursive integers(i) as ( 8 | select 0 as i 9 | union all 10 | select i + 1 11 | from integers 12 | where i + 1 < 100 13 | ) 14 | select 15 | 'S' || (i%10) as series, 16 | format('%d-01-01', 2010 + (i/10)) as x, 17 | abs(random() % 10) as value 18 | from integers; 19 | -------------------------------------------------------------------------------- /examples/official-site/examples/chart.sql: -------------------------------------------------------------------------------- 1 | set n=coalesce($n, 1); 2 | 3 | select 4 | 'chart' as component, 5 | 'Syracuse Sequence' as title, 6 | coalesce($type, 'area') as type, 7 | coalesce($color, 'indigo') as color, 8 | 5 as marker, 9 | 0 as ymin; 10 | with recursive seq(x, y) as ( 11 | select 0, CAST($n as integer) 12 | union all 13 | select x+1, case 14 | when y % 2 = 0 then y/2 15 | else 3*y+1 16 | end 17 | from seq 18 | where x<10 19 | ) 20 | select x, y from seq; 21 | 22 | -------------------------------------------------------------------------------- /examples/official-site/examples/csv_download.sql: -------------------------------------------------------------------------------- 1 | select 'csv' as component, 'example.csv' as filename; 2 | select * from component; 3 | -------------------------------------------------------------------------------- /examples/official-site/examples/from_component_options_source.sql: -------------------------------------------------------------------------------- 1 | select 'json' as component; 2 | 3 | select name as value, name as label 4 | from component 5 | where name like '%' || $search || '%'; -------------------------------------------------------------------------------- /examples/official-site/examples/handle_csv_upload.sql: -------------------------------------------------------------------------------- 1 | -- temporarily store the data in a table with text columns 2 | create temporary table if not exists product_tmp(name text, description text, price text); 3 | delete from product_tmp; 4 | 5 | -- copy the data from the CSV file into the temporary table 6 | copy product_tmp(name, description, price) from 'product_data_file'; 7 | 8 | select 'table' as component; 9 | select * from product_tmp; -------------------------------------------------------------------------------- /examples/official-site/examples/handle_enctype.sql: -------------------------------------------------------------------------------- 1 | SET ":enctype" = CASE :percent_encoded IS NOT NULL OR :multipart_form_data IS NOT NULL 2 | WHEN TRUE THEN 'with ``' || COALESCE(:percent_encoded, :multipart_form_data) || '``' 3 | ELSE 'form' 4 | END ||' encoding type' 5 | SELECT 'text' AS component; 6 | SELECT 'The following data was submitted '||:enctype||': 7 | ``` 8 | ' || :data ||' 9 | ```' AS contents_md; -------------------------------------------------------------------------------- /examples/official-site/examples/index.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, '/examples/tabs' as link; -------------------------------------------------------------------------------- /examples/official-site/examples/show_variables.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' as component, 'SQLPage' as title, 2 | 'chart' as menu_item, 3 | 'layouts' as menu_item, 4 | 'tabs' as menu_item, 5 | 'show_variables' as menu_item; 6 | 7 | select 'list' as component, 'POST variables' as title, 8 | 'Here is the list of POST variables sent to this page. 9 | Post variables are accessible with `:variable_name`.' as description_md, 10 | 'No POST variable.' as empty_title; 11 | select key as title, ':' || key || ' = ' || "value" as description 12 | from json_each(sqlpage.variables('post')); 13 | 14 | select 'list' as component, 'GET variables' as title, 15 | 'Here is the list of GET variables sent to this page. 16 | Get variables are accessible with `$variable_name`.' as description_md, 17 | 'No GET variable.' as empty_title; 18 | select key as title, '$' || key || ' = ' || "value" as description 19 | from json_each(sqlpage.variables('get')); -------------------------------------------------------------------------------- /examples/official-site/examples/tabs.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, 'tabs/' as link; -------------------------------------------------------------------------------- /examples/official-site/extensions-to-sql.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; 3 | 4 | select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; 5 | 6 | -- Article by Matthew Larkin 7 | select 'text' as component, 8 | sqlpage.read_file_as_text('extensions-to-sql.md') as contents_md, 9 | true as article; 10 | -------------------------------------------------------------------------------- /examples/official-site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/favicon.ico -------------------------------------------------------------------------------- /examples/official-site/get started.sql: -------------------------------------------------------------------------------- 1 | SELECT 'redirect' as component, 'your-first-sql-website/' as link; 2 | -------------------------------------------------------------------------------- /examples/official-site/get_started.sql: -------------------------------------------------------------------------------- 1 | SELECT 'redirect' as component, 'your-first-sql-website/' as link; 2 | -------------------------------------------------------------------------------- /examples/official-site/index.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", 3 | '; rel="canonical"' as "Link"; 4 | 5 | select 'shell-home' as component; 6 | -------------------------------------------------------------------------------- /examples/official-site/manual_setup.sql: -------------------------------------------------------------------------------- 1 | SELECT 'redirect' as component, 'get started.sql' as link; -------------------------------------------------------------------------------- /examples/official-site/performance.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/performance.webp -------------------------------------------------------------------------------- /examples/official-site/pgconf/2024-sqlpage-badass.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/pgconf/2024-sqlpage-badass.pdf -------------------------------------------------------------------------------- /examples/official-site/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /examples/authentication/basic_auth.sql 3 | Disallow: /cdn-cgi/l/email-protection 4 | Disallow: /sqlpage/ 5 | Crawl-delay: 1 6 | -------------------------------------------------------------------------------- /examples/official-site/rss.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'application/rss+xml' as "Content-Type"; 3 | select 'shell-empty' as component; 4 | select 'rss' as component, 5 | 'SQLPage blog' as title, 6 | 'https://sql-page.com/blog.sql' as link, 7 | 'latest news about SQLpage' as description, 8 | 'en' as language, 9 | 'https://sql-page.com/rss.sql' as self_link, 10 | 'Technology' as category, 11 | '2de3f968-9928-5ec6-9653-6fc6fe382cfd' as guid; 12 | SELECT title, 13 | description, 14 | CASE 15 | WHEN external_url IS NOT NULL THEN external_url 16 | ELSE 'https://sql-page.com/blog.sql?post=' || title 17 | END AS link, 18 | created_at AS date, 19 | false AS explicit 20 | FROM blog_posts 21 | ORDER BY created_at DESC; -------------------------------------------------------------------------------- /examples/official-site/safety.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/safety.webp -------------------------------------------------------------------------------- /examples/official-site/sqlpage/migrations/21_path.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO sqlpage_functions ( 2 | "name", 3 | "introduced_in_version", 4 | "icon", 5 | "description_md" 6 | ) 7 | VALUES ( 8 | 'path', 9 | '0.15.0', 10 | 'slashes', 11 | 'Returns the request path of the current page. 12 | This is useful to generate links to the current page, and when you have a proxy in front of your SQLPage server that rewrites the URL. 13 | 14 | ### Example 15 | 16 | If we have a page in a file named `my page.sql` at the root of your SQLPage installation 17 | then the following SQL query: 18 | 19 | ```sql 20 | select ''text'' as component, sqlpage.path() as contents; 21 | ``` 22 | 23 | will return `/my%20page.sql`. 24 | 25 | > Note that the path is URL-encoded. 26 | '); -------------------------------------------------------------------------------- /examples/official-site/sqlpage/migrations/27_protocol.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO sqlpage_functions ( 2 | "name", 3 | "introduced_in_version", 4 | "icon", 5 | "description_md" 6 | ) 7 | VALUES ( 8 | 'protocol', 9 | '0.17.1', 10 | 'network', 11 | 'Returns the protocol that was used to access the current page. 12 | 13 | This can be either `http` or `https`. 14 | 15 | This is useful to generate links to the current page. 16 | 17 | ### Example 18 | 19 | ```sql 20 | select ''text'' as component, 21 | sqlpage.protocol() || ''://'' || sqlpage.header(''host'') || sqlpage.path() as contents; 22 | ``` 23 | 24 | will return `https://example.com/example.sql`. 25 | 26 | > Note that the path is URL-encoded. The protocol is resolved in this order: 27 | > - `Forwarded` header 28 | > - `X-Forwarded-Proto` header 29 | > request target / URI 30 | '); -------------------------------------------------------------------------------- /examples/official-site/sqlpage/migrations/44_authentication_example.sql: -------------------------------------------------------------------------------- 1 | create table users ( 2 | username text primary key, 3 | password_hash text not null, 4 | role text not null 5 | ); 6 | 7 | -- Create example users with trivial passwords for the website's demo 8 | insert into users (username, password_hash, role) 9 | values 10 | ('admin', '$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$ROyXNhK0utkzTA', 'admin'), -- password: admin 11 | ('user', '$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$qsrWdjgl96ooYw', 'user'); -- password: user 12 | -- (the password hashes can be generated using the `sqlpage.hash_password` function) 13 | 14 | create table user_sessions ( 15 | session_token text primary key, 16 | username text not null references users(username), 17 | created_at timestamp not null default current_timestamp 18 | ); -------------------------------------------------------------------------------- /examples/official-site/sqlpage/migrations/59_unsafe_contents_md.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'text', * FROM (VALUES 2 | ('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', TRUE, TRUE), 3 | ('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', FALSE, TRUE) 4 | ); 5 | -------------------------------------------------------------------------------- /examples/official-site/sqlpage/sqlpage.yaml: -------------------------------------------------------------------------------- 1 | # The documentation site is fully static, so we don't need to persist any data. 2 | database_url: "sqlite::memory:?cache=shared" 3 | 4 | # We have a file upload example, and would like to limit the size of the uploaded files 5 | max_uploaded_file_size: 256000 6 | 7 | database_connection_acquire_timeout_seconds: 30 -------------------------------------------------------------------------------- /examples/official-site/sqlpage_cover_image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/sqlpage_cover_image.webp -------------------------------------------------------------------------------- /examples/official-site/sqlpage_illustration_alien.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/sqlpage_illustration_alien.webp -------------------------------------------------------------------------------- /examples/official-site/sqlpage_illustration_components.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/sqlpage_illustration_components.webp -------------------------------------------------------------------------------- /examples/official-site/sqlpage_introduction_video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/sqlpage_introduction_video.webm -------------------------------------------------------------------------------- /examples/official-site/sqlpage_social_preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/sqlpage_social_preview.webp -------------------------------------------------------------------------------- /examples/official-site/sso/index.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control", 3 | '; rel="canonical"' as "Link"; 4 | 5 | select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; 6 | 7 | select 'text' as component, sqlpage.read_file_as_text('sso/single_sign_on.md') as contents_md, true as article; -------------------------------------------------------------------------------- /examples/official-site/style_pricing.css: -------------------------------------------------------------------------------- 1 | /* Ensure all unordered and ordered lists are left-aligned */ 2 | ul, 3 | ol { 4 | text-align: left; /* Aligns bullet points to the left */ 5 | margin-left: 20px; /* Adds indentation for better readability */ 6 | padding-left: 20px; /* Adds space between bullet and text */ 7 | } 8 | 9 | /* Optional: Style for the individual list items */ 10 | li { 11 | margin-bottom: 10px; /* Adds space between list items */ 12 | } 13 | 14 | /* Optional: Ensure the body text is also left-aligned */ 15 | body { 16 | text-align: left; /* Makes sure the overall page is left-aligned */ 17 | } 18 | -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/documentation.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, '../documentation.sql' as link; -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/final-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/final-result.png -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/first-sql-website-launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/first-sql-website-launch.png -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/full-website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/full-website.png -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/get_started.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/get_started.webp -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/get_started_linux.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/get_started_linux.webp -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/get_started_macos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/get_started_macos.webp -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/get_started_windows.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/get_started_windows.webp -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/hello-world.png -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/migrations.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; 3 | 4 | select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; 5 | 6 | -- Article by Matthew Larkin 7 | select 'text' as component, 8 | sqlpage.read_file_as_text('your-first-sql-website/migrations.md') as contents_md, 9 | true as article; 10 | -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/nginx.sql: -------------------------------------------------------------------------------- 1 | select 'http_header' as component, 2 | 'public, max-age=300, stale-while-revalidate=3600, stale-if-error=86400' as "Cache-Control"; 3 | 4 | select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1; 5 | select 'text' as component, sqlpage.read_file_as_text('your-first-sql-website/nginx.md') as contents_md; -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/not_found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/not_found.jpg -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/sql.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/official-site/your-first-sql-website/sql.webp -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/tutorial-install-macos.md: -------------------------------------------------------------------------------- 1 | # Download SQLPage for Mac OS 2 | 3 | On Mac OS, Apple blocks the execution of downloaded files by default. The easiest way to run SQLPage is to use [Homebrew](https://brew.sh). 4 | Open a terminal and run the following commands: 5 | 6 | ```sh 7 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 8 | brew install sqlpage 9 | sqlpage 10 | ``` 11 | 12 | > **Note**: Advanced users can alternatively install SQLPage using 13 | > [the precompiled binaries](https://github.com/sqlpage/SQLPage/releases/latest), 14 | > [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general), 15 | > [nix](https://search.nixos.org/packages?channel=unstable&show=sqlpage), 16 | > or [cargo](https://crates.io/crates/sqlpage). 17 | 18 | > **Not on Mac OS?** See the instructions for [Windows](?os=windows#download), or for [Other Systems](?os=any#download). -------------------------------------------------------------------------------- /examples/official-site/your-first-sql-website/tutorial-install-windows.md: -------------------------------------------------------------------------------- 1 | # Download the SQLPage executable for Windows 2 | 3 | SQLPage offers a small executable file (`sqlpage.exe`) that will take requests to your website, 4 | execute the SQL files you write, and render the database responses as nice web pages. 5 | 6 | [Download the latest SQLPage for Windows](https://github.com/sqlpage/SQLPage/releases/latest/download/sqlpage-windows.zip). 7 | Download the file, and extract the executable file from the zip archive. 8 | 9 | > **Note**: Advanced users can alternatively install SQLPage using 10 | > [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general), 11 | > [scoop](https://scoop.sh/#/apps?q=sqlpage&id=305b3437817cd197058954a2f76ac1cf0e444116), 12 | > or [cargo](https://crates.io/crates/sqlpage). 13 | 14 | > **Not on Windows?** See the instructions for [Mac OS](?os=macos#download), or for [Other Systems](?os=any#download). -------------------------------------------------------------------------------- /examples/plots tables and forms/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage demo 2 | 3 | This short demo illustrates the usage of 4 | - plots 5 | - tables 6 | - forms 7 | - interactivity to filter data based on an URL parameter 8 | - debugging using the `debug` component 9 | 10 | The [`index.sql`](./index.sql) page is heavily commented to explain the different components and concepts. -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lovasoa/sqlpage 2 | 3 | COPY *.sql sqlpage ./ -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage application with custom login in Postgres 2 | 3 | This is a very simple example of a website that uses the SQLPage web application framework. It uses a Postgres database for storing the data. 4 | 5 | It lets a user log in and out, and it shows a list of the users that have logged in. -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/index.sql: -------------------------------------------------------------------------------- 1 | -- Sets the username cookie to the value of the username parameter 2 | SELECT 'cookie' as component, 3 | 'username' as name, 4 | $username as value 5 | WHERE $username IS NOT NULL; 6 | 7 | SELECT 'form' as component; 8 | SELECT 'username' as name, 9 | 'User Name' as label, 10 | COALESCE($username, sqlpage.cookie('username')) as value, 11 | 'try leaving this page and coming back, the value should be saved in a cookie' as description; 12 | 13 | select 'text' as component; 14 | select 'log out' as contents, 'logout.sql' as link; 15 | 16 | select 'text' as component; 17 | select 'View the cookie from a subdirectory' as contents, 'subdirectory/read_cookies.sql' as link; -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/logout.sql: -------------------------------------------------------------------------------- 1 | -- Remove the username cookie 2 | SELECT 'cookie' as component, 3 | 'username' as name, 4 | TRUE as remove; 5 | 6 | SELECT 'redirect' as component, 'index.sql' as link; -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "sqlite://:memory:" 3 | } 4 | -------------------------------------------------------------------------------- /examples/read-and-set-http-cookies/subdirectory/read_cookies.sql: -------------------------------------------------------------------------------- 1 | -- Cookies can be specific to a certain path in the website 2 | -- This page demonstrates that a gobal cookie can be removed from a subdirectory 3 | select 'cookie' as component, 'username' as name, true as remove, '/' as path; 4 | 5 | SELECT 'text' as component; 6 | SELECT 'The value of your username cookie was: ' || 7 | COALESCE(sqlpage.cookie('username'), 'NULL') || 8 | '. It has now been removed. You can reload this page.' as contents; 9 | 10 | -------------------------------------------------------------------------------- /examples/rich-text-editor/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage rich text editor 2 | 3 | This demo shows how to build an application where users can 4 | input and safely store rich text (with titles, bold, italics, and images). 5 | 6 | ![image](https://github.com/user-attachments/assets/47e43ee2-b7cb-4d72-a244-4a8885b51577) 7 | -------------------------------------------------------------------------------- /examples/rich-text-editor/create_blog_post.sql: -------------------------------------------------------------------------------- 1 | insert into blog_posts (title, content) 2 | values (:title, :content) 3 | returning 4 | 'redirect' as component, 5 | 'post?id=' || id as link; -------------------------------------------------------------------------------- /examples/rich-text-editor/index.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | 'Rich text editor' as title, 3 | '/rich_text_editor.js' as javascript_module; 4 | 5 | 6 | select 'form' as component, 7 | 'Create a new blog post' as title, 8 | 'create_blog_post' as action, 9 | 'Create' as validate; 10 | 11 | select 'title' as name, 'Blog post title' as label, 'My new post' as value; 12 | select 'content' as name, 'textarea' as type, 'Your blog post here' as label, 'Your blog post here' as value, true as required; 13 | 14 | select 'list' as component, 15 | 'Blog posts' as title; 16 | 17 | select title, sqlpage.link('post', json_object('id', id)) as link 18 | from blog_posts; 19 | 20 | -------------------------------------------------------------------------------- /examples/rich-text-editor/post.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | title 3 | from blog_posts 4 | where id = $id; 5 | 6 | select 'text' as component, 7 | true as article, 8 | content as contents_md 9 | from blog_posts 10 | where id = $id; 11 | 12 | select 'list' as component; 13 | select 14 | 'Edit' as title, 15 | 'pencil' as icon, 16 | 'edit?id=' || $id as link; 17 | -------------------------------------------------------------------------------- /examples/rich-text-editor/sqlpage/migrations/01_blog_posts.sql: -------------------------------------------------------------------------------- 1 | create table blog_posts ( 2 | id integer primary key autoincrement, 3 | title text not null, 4 | content text not null 5 | ); -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lovasoa/sqlpage:latest 2 | 3 | COPY sqlpage /etc/sqlpage 4 | COPY src /var/www 5 | ENV DATABASE_URL=sqlite:///tmp/pokemon.db?mode=rwc 6 | -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/README.md: -------------------------------------------------------------------------------- 1 | # Roundest (SQLPage Version) 2 | 3 | This is a simple web app that allows you to vote on which Pokemon is the most round. 4 | 5 | | Vote UI | Results UI | 6 | | --- | --- | 7 | | ![vote ui screenshot](screenshots/vote.png) | ![results ui](screenshots/results.png) | 8 | 9 | It demonstrates how to build an entirely custom web app, 10 | without using any of the pre-built components of SQLPage. 11 | 12 | All the custom components are in the [`sqlpage/templates/`](./sqlpage/templates/) folder. 13 | 14 | ## Running the app 15 | 16 | ### Using an installed version of SQLPage 17 | 18 | ``` 19 | sqlpage --web-root src 20 | ``` 21 | 22 | ### Using Docker 23 | 24 | ``` 25 | docker build -t roundest-sqlpage . 26 | docker run -p 8080:8080 -it roundest-sqlpage 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/roundest_pokemon_rating/screenshots/results.png -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/screenshots/vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/roundest_pokemon_rating/screenshots/vote.png -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/sqlpage/migrations/0000_pokemon_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS pokemon ( 2 | dex_id INTEGER PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | up_votes INTEGER DEFAULT 0, 5 | down_votes INTEGER DEFAULT 0 6 | ); -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/roundest_pokemon_rating/src/favicon.ico -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/src/index.sql: -------------------------------------------------------------------------------- 1 | select 'redirect' as component, '/populate.sql' as link 2 | where not exists(select 1 from pokemon); 3 | 4 | select 'pokemon' as component; 5 | 6 | select dex_id as dexNumber, name 7 | from pokemon 8 | order by random() limit 2; 9 | -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/src/populate.sql: -------------------------------------------------------------------------------- 1 | -- This updates our pokemon table with fresh data from pokeapi.co 2 | insert into pokemon (dex_id, name) 3 | select 4 | cast(rtrim(substr(value->>'url', 5 | length('https://pokeapi.co/api/v2/pokemon/') + 1), 6 | '/') as integer) as dex_id, 7 | value->>'name' as name 8 | from json_each( 9 | sqlpage.fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0') -> 'results' 10 | ); 11 | 12 | select 'redirect' as component, '/' as link; -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/src/results.sql: -------------------------------------------------------------------------------- 1 | select 'results' as component; 2 | 3 | with ranked as ( 4 | select 5 | *, 6 | case 7 | when (up_votes + down_votes) = 0 then 0 8 | else 100 * up_votes / (up_votes + down_votes) 9 | end as win_percentage, 10 | up_votes - down_votes as score 11 | from pokemon 12 | ) 13 | select *, rank() over (order by score desc) as rank 14 | from ranked 15 | order by win_percentage desc, score desc; 16 | -------------------------------------------------------------------------------- /examples/roundest_pokemon_rating/src/vote.sql: -------------------------------------------------------------------------------- 1 | update pokemon 2 | set 3 | up_votes = up_votes + (dex_id = $voted), 4 | down_votes = down_votes + (dex_id != $voted) 5 | where dex_id IN (:option_0, :option_1); 6 | 7 | select 'redirect' as component, '/' as link; 8 | -------------------------------------------------------------------------------- /examples/sending emails/index.sql: -------------------------------------------------------------------------------- 1 | select 'form' as component, 'Send an email' as title, 'email.sql' as action; 2 | 3 | select 'to_email' as name, 'To email' as label, 'recipient@example.com' as value; 4 | select 'subject' as name, 'Subject' as label, 'Test email' as value; 5 | select 'textarea' as type, 'message_text' as name, 'Message' as label, 'This is a test email' as value; 6 | -------------------------------------------------------------------------------- /examples/simple-website-example/delete.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users 2 | WHERE id = $id 3 | RETURNING 4 | 'text' AS component, 5 | ' 6 | The user ' || username || ' has been deleted. 7 | 8 | [Back to the user list](index.sql)' as contents_md; -------------------------------------------------------------------------------- /examples/simple-website-example/edit.sql: -------------------------------------------------------------------------------- 1 | update users 2 | set username = :Username, 3 | is_admin = :Administrator is not null 4 | where :Username is not null and id = $id; 5 | 6 | select 'form' as component; 7 | select 'text' as type, 'Username' as name, username as value from users where id = $id; 8 | select 'checkbox' as type, 'Has administrator privileges' as label, 'Administrator' as name, is_admin as checked from users where id = $id; -------------------------------------------------------------------------------- /examples/simple-website-example/index.sql: -------------------------------------------------------------------------------- 1 | -- A form to create a new entry in the database 2 | SELECT 'form' AS component, 3 | 'Add a user' AS title; 4 | SELECT 'Username' as name, 5 | TRUE as required; 6 | -- Handle the form results when present 7 | INSERT INTO users (username) 8 | SELECT :Username 9 | WHERE :Username IS NOT NULL; 10 | ---------------------------------- 11 | -- Display the list of users 12 | -- It is important that this query comes after the INSERT query above, 13 | -- so that the updated list is visible immediately 14 | SELECT 'list' AS component, 15 | 'Users' AS title; 16 | SELECT username AS title, 17 | username || ' is a user on this website.' as description, 18 | case when is_admin then 'red' end as color, 19 | 'user' as icon, 20 | 'user.sql?id=' || id as link 21 | FROM users; -------------------------------------------------------------------------------- /examples/simple-website-example/sqlpage/migrations/0001_create_users_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | username TEXT NOT NULL, 4 | is_admin BOOLEAN NOT NULL DEFAULT FALSE 5 | ); -------------------------------------------------------------------------------- /examples/simple-website-example/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "sqlite://:memory:" 3 | } 4 | -------------------------------------------------------------------------------- /examples/simple-website-example/table.sql: -------------------------------------------------------------------------------- 1 | select 'table' as component, 'action' as markdown; 2 | select *, 3 | format('[Edit](edit.sql?id=%s)', id) as action 4 | from users; -------------------------------------------------------------------------------- /examples/simple-website-example/user.sql: -------------------------------------------------------------------------------- 1 | SELECT 'text' as component, 2 | username as title, 3 | username || ' is a user on this site. 4 | 5 | [Delete this user](delete.sql?id=' || id || ') 6 | 7 | [Edit user](edit.sql?id=' || id || ')' as contents_md 8 | FROM users 9 | WHERE id = $id; -------------------------------------------------------------------------------- /examples/single sign on/assets/closed.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/closed.jpeg -------------------------------------------------------------------------------- /examples/single sign on/assets/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/homepage.png -------------------------------------------------------------------------------- /examples/single sign on/assets/keycloak_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/keycloak_configuration.png -------------------------------------------------------------------------------- /examples/single sign on/assets/logged_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/logged_in.png -------------------------------------------------------------------------------- /examples/single sign on/assets/login_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/login_page.png -------------------------------------------------------------------------------- /examples/single sign on/assets/welcome.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/single sign on/assets/welcome.jpeg -------------------------------------------------------------------------------- /examples/single sign on/cas/index.sql: -------------------------------------------------------------------------------- 1 | set user_email = (select email from user_sessions where session_id = sqlpage.cookie('session_id')); 2 | 3 | select 'text' as component, 'You are not authenticated. [Log in](login.sql).' as contents_md where $user_email is null; 4 | select 'text' as component, 'Welcome, ' || $user_email || '. You can now [log out](logout.sql).' as contents_md where $user_email is not null; 5 | -------------------------------------------------------------------------------- /examples/single sign on/cas/login.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'redirect' as component, 3 | sqlpage.environment_variable('CAS_ROOT_URL') 4 | || '/login?service=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/cas/redirect_handler.sql' 5 | as link; -------------------------------------------------------------------------------- /examples/single sign on/cas/logout.sql: -------------------------------------------------------------------------------- 1 | -- remove the session cookie 2 | select 'cookie' as component, 'session_id' as name, true as remove; 3 | -- remove the session from the database 4 | delete from user_sessions where session_id = sqlpage.cookie('session_id'); 5 | -- log the user out of the cas server 6 | select 7 | 'redirect' as component, 8 | sqlpage.environment_variable('CAS_ROOT_URL') 9 | || '/logout?service=' || sqlpage.protocol() || '://' || sqlpage.header('host') || '/cas/redirect_handler.sql' 10 | as link; -------------------------------------------------------------------------------- /examples/single sign on/index.sql: -------------------------------------------------------------------------------- 1 | set user_email = sqlpage.user_info('email'); 2 | 3 | select 'shell' as component, 'My secure app' as title, 4 | 'logout' as menu_item; 5 | 6 | select 'text' as component, 7 | 'You''re in !' as title, 8 | 'You are logged in as *`' || $user_email || '`*. 9 | You have access to the [protected page](protected.sql). 10 | 11 | ![open door](/assets/welcome.jpeg)' 12 | as contents_md; 13 | 14 | select 'list' as component; 15 | select key as title, value as description 16 | from json_each(sqlpage.id_token()); -------------------------------------------------------------------------------- /examples/single sign on/keycloak.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM keycloak/keycloak:24.0 2 | 3 | ADD --chown=1000:0 https://github.com/jacekkow/keycloak-protocol-cas/releases/download/24.0.3/keycloak-protocol-cas-24.0.3.jar \ 4 | /opt/keycloak/providers/keycloak-protocol-cas.jar 5 | 6 | COPY ./keycloak-configuration.json /opt/keycloak/data/import/realm.json 7 | 8 | CMD ["start-dev", "--import-realm", "--http-port", "8181"] -------------------------------------------------------------------------------- /examples/single sign on/logout.sql: -------------------------------------------------------------------------------- 1 | -- remove the session cookie 2 | select 3 | 'cookie' as component, 4 | 'sqlpage_auth' as name, 5 | true as remove; 6 | 7 | select 8 | 'redirect' as component, 9 | 'http://localhost:8181/realms/sqlpage_demo/protocol/openid-connect/logout' as link; -------------------------------------------------------------------------------- /examples/single sign on/protected.sql: -------------------------------------------------------------------------------- 1 | select 'card' as component, 'My secure protected page' as title, 1 as columns; 2 | 3 | select 4 | 'Secret video' as title, 5 | 'https://www.youtube.com/embed/mXdgmSdaXkg' as embed, 6 | 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' as allow, 7 | 'iframe' as embed_mode, 8 | '700' as height; -------------------------------------------------------------------------------- /examples/single sign on/sqlpage/migrations/000_sessions.sql: -------------------------------------------------------------------------------- 1 | -- Table to store user sessions 2 | CREATE TABLE user_sessions( 3 | session_id TEXT PRIMARY KEY, 4 | user_id TEXT NOT NULL, 5 | email TEXT NOT NULL, 6 | oidc_token TEXT NOT NULL, 7 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 8 | ) -------------------------------------------------------------------------------- /examples/single sign on/sqlpage/sqlpage.yaml: -------------------------------------------------------------------------------- 1 | oidc_issuer_url: http://localhost:8181/realms/sqlpage_demo 2 | oidc_client_id: sqlpage 3 | oidc_client_secret: qiawfnYrYzsmoaOZT28rRjPPRamfvrYr # For a safer setup, use environment variables to store this 4 | -------------------------------------------------------------------------------- /examples/splitwise/README.md: -------------------------------------------------------------------------------- 1 | # SQL-only expense tracker app 2 | 3 | This is a small web application that allows you to track shared expenses with your friends. 4 | It is built entirely in SQL using SQLPage. 5 | 6 | ![screenshot](../../docs/example-splitwise.png) -------------------------------------------------------------------------------- /examples/splitwise/index.sql: -------------------------------------------------------------------------------- 1 | -- Simple form to create a shared expense account 2 | SELECT 'form' as component, 3 | 'New shared expense account' as title, 4 | 'Create the shared expense account!' as validate; 5 | SELECT 'Account Name' AS label, 6 | 'shared_expense_name' AS name; 7 | 8 | -- Insert the shared expense account posted by the form into the database 9 | INSERT INTO expense_group(name) 10 | SELECT :shared_expense_name 11 | WHERE :shared_expense_name IS NOT NULL; 12 | 13 | -- List of shared expense accounts 14 | -- (we put it after the insertion because we want to see new accounts right away when they are created) 15 | SELECT 'list' as component; 16 | SELECT name AS title, 17 | 'group.sql?id=' || id AS link 18 | FROM expense_group; 19 | -------------------------------------------------------------------------------- /examples/splitwise/sqlpage/migrations/0000_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE expense_group( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | name TEXT 4 | ); 5 | CREATE TABLE group_member( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | group_id INTEGER REFERENCES expense_group(id), 8 | name TEXT 9 | ); 10 | CREATE TABLE expense( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT, 12 | spent_by INTEGER REFERENCES group_member(id), 13 | date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 14 | name TEXT, 15 | amount DECIMAL 16 | ); -------------------------------------------------------------------------------- /examples/tiny_twitter/.gitignore: -------------------------------------------------------------------------------- 1 | sqlpage.bin 2 | -------------------------------------------------------------------------------- /examples/tiny_twitter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lovasoa/sqlpage:main 2 | 3 | COPY sqlpage /etc/sqlpage 4 | COPY . /var/www/ -------------------------------------------------------------------------------- /examples/tiny_twitter/README.md: -------------------------------------------------------------------------------- 1 | # Tiny Tweeter 2 | 3 | This is a simple example of a very simple Twitter-like application running on top of PostgreSQL. 4 | It is called tweeter because Elon Musk already has the Twitter trademark, even though he doesn't use it. 5 | 6 | It was presented at the [2023 PGConf.EU](https://2023.pgconf.eu/) conference. 7 | 8 | You can find the slides at https://sql-page.com/pgconf/pgconf-2023.html. -------------------------------------------------------------------------------- /examples/tiny_twitter/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sqlpage: 3 | build: . 4 | ports: ["8080:8080"] 5 | volumes: [".:/var/www"] 6 | depends_on: [postgres] 7 | environment: 8 | - DATABASE_URL=postgres://root:root@postgres/sqlpage 9 | postgres: 10 | image: postgres:16 11 | ports: ["5432:5432"] 12 | environment: 13 | - POSTGRES_USER=root 14 | - POSTGRES_PASSWORD=root 15 | - POSTGRES_DB=sqlpage -------------------------------------------------------------------------------- /examples/tiny_twitter/index.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | 'TinyTweeter' as title; 3 | 4 | select 'form' as component, 5 | 'Tweet' as validate; 6 | select 'new_tweet' as name, 7 | 'Your story' as label, 8 | 'textarea' as type, 9 | 'Tell me your story...' as placeholder; 10 | select 'checkbox' as type, 11 | 'Terms and conditions' as label, 12 | true as required; 13 | 14 | insert into tweets (tweet) 15 | select :new_tweet 16 | where :new_tweet is not null; 17 | 18 | select 'card' as component, 19 | 'Tweets' as title, 20 | 1 as columns; 21 | select tweet as description 22 | from tweets; -------------------------------------------------------------------------------- /examples/tiny_twitter/sqlpage/migrations/0001_tweets.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tweets( 2 | id bigserial, 3 | tweet text 4 | ); -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version 4 | ports: 5 | - "8080:8080" 6 | volumes: 7 | - .:/var/www 8 | - ./sqlpage:/etc/sqlpage 9 | depends_on: 10 | - db 11 | environment: 12 | DATABASE_URL: postgres://root:secret@db/sqlpage 13 | db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases 14 | ports: 15 | - "5432:5432" 16 | - "3306:3306" 17 | image: postgres 18 | environment: 19 | POSTGRES_USER: root 20 | POSTGRES_DB: sqlpage 21 | POSTGRES_PASSWORD: secret 22 | -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/index.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; 2 | 3 | select 'list' as component, 4 | 'Todo' as title, 5 | 'No todo yet...' as empty_title; 6 | 7 | select 8 | title, 9 | 'todo_form.sql?todo_id=' || id as edit_link, 10 | 'delete.sql?todo_id=' || id as delete_link 11 | from todos; 12 | 13 | select 14 | 'button' as component, 15 | 'center' as justify; 16 | select 17 | 'todo_form.sql' as link, 18 | 'green' as color, 19 | 'Add new todo' as title, 20 | 'circle-plus' as icon; -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/todo application (PostgreSQL)/screenshot.png -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/shell.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'shell' as component, 3 | format ('Todo list (%s)', count(*)) as title, 4 | 'batch' as menu_item, 5 | 'timeline' as menu_item 6 | from 7 | todos; -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/sqlpage/migrations/0000_init.sql: -------------------------------------------------------------------------------- 1 | create table 2 | todos ( 3 | id serial primary key, 4 | title text not null, 5 | created_at timestamp default current_timestamp 6 | ); -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/sqlpage/templates/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage component templates 2 | 3 | SQLPage templates are handlebars[^1] files that are used to render the results of SQL queries. 4 | 5 | [^1]: https://handlebarsjs.com/ 6 | 7 | ## Default components 8 | 9 | SQLPage comes with a set of default[^2] components that you can use without having to write any code. 10 | These are documented on https://sql-page.com/components.sql 11 | 12 | ## Custom components 13 | 14 | You can [write your own component templates](https://sql-page.com/custom_components.sql) 15 | and place them in the `sqlpage/templates` directory. 16 | To override a default component, create a file with the same name as the default component. 17 | If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory 18 | in the SQLPage source code[^2]. 19 | 20 | [^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars -------------------------------------------------------------------------------- /examples/todo application (PostgreSQL)/timeline.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'dynamic' as component, 3 | sqlpage.run_sql ('shell.sql') as properties; 4 | 5 | select 6 | 'timeline' as component; 7 | 8 | SELECT 9 | title, 10 | 'todo_form.sql?todo_id=' || id AS link, 11 | TO_CHAR (created_at, 'FMMonth DD, YYYY, HH12:MI AM TZ') AS date, 12 | 'calendar' AS icon, 13 | 'green' AS color, 14 | CONCAT ( 15 | EXTRACT( 16 | DAY 17 | FROM 18 | NOW () - created_at 19 | ), 20 | ' days ago' 21 | ) AS description 22 | FROM 23 | todos 24 | ORDER BY 25 | created_at DESC; -------------------------------------------------------------------------------- /examples/todo application/index.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; 2 | 3 | select 'list' as component, 4 | 'Todo' as title, 5 | 'No todo yet...' as empty_title; 6 | 7 | select 8 | title, 9 | 'todo_form.sql?todo_id=' || id as edit_link, 10 | 'delete.sql?todo_id=' || id as delete_link 11 | from todos; 12 | 13 | select 14 | 'button' as component, 15 | 'center' as justify; 16 | select 17 | 'todo_form.sql' as link, 18 | 'green' as color, 19 | 'Add new todo' as title, 20 | 'circle-plus' as icon; -------------------------------------------------------------------------------- /examples/todo application/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/todo application/screenshot.png -------------------------------------------------------------------------------- /examples/todo application/shell.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | printf('Todo list (%d)', count(*)) as title, 3 | 'timeline' as menu_item 4 | from todos; -------------------------------------------------------------------------------- /examples/todo application/sqlpage/migrations/0000_init.sql: -------------------------------------------------------------------------------- 1 | create table todos( 2 | id integer primary key, 3 | title text not null, 4 | created_at timestamp default current_timestamp 5 | ); -------------------------------------------------------------------------------- /examples/todo application/sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "sqlite://./sqlpage/sqlpage.db?mode=rwc" 3 | } 4 | -------------------------------------------------------------------------------- /examples/todo application/sqlpage/templates/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage component templates 2 | 3 | SQLPage templates are handlebars[^1] files that are used to render the results of SQL queries. 4 | 5 | [^1]: https://handlebarsjs.com/ 6 | 7 | ## Default components 8 | 9 | SQLPage comes with a set of default[^2] components that you can use without having to write any code. 10 | These are documented on https://sql-page.com/components.sql 11 | 12 | ## Custom components 13 | 14 | You can [write your own component templates](https://sql-page.com/custom_components.sql) 15 | and place them in the `sqlpage/templates` directory. 16 | To override a default component, create a file with the same name as the default component. 17 | If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory 18 | in the SQLPage source code[^2]. 19 | 20 | [^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars -------------------------------------------------------------------------------- /examples/todo application/timeline.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties; 2 | 3 | select 4 | 'timeline' as component; 5 | select 6 | title, 7 | 'todo_form.sql?todo_id=' || id as link, 8 | created_at as date, 9 | 'calendar' as icon, 10 | 'green' as color, 11 | printf('%d days ago', julianday('now') - julianday(created_at)) as description 12 | from todos 13 | order by created_at desc; -------------------------------------------------------------------------------- /examples/user-authentication/create_user.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user_info (username, password_hash) 2 | VALUES (:username, sqlpage.hash_password(:password)) 3 | ON CONFLICT (username) DO NOTHING 4 | RETURNING 5 | 'redirect' AS component, 6 | 'create_user_welcome_message.sql?username=' || :username AS link; 7 | 8 | -- If we are still here, it means that the user was not created 9 | -- because the username was already taken. 10 | SELECT 'redirect' AS component, 'create_user_welcome_message.sql?error&username=' || :username AS link; 11 | -------------------------------------------------------------------------------- /examples/user-authentication/create_user_welcome_message.sql: -------------------------------------------------------------------------------- 1 | SELECT 'hero' AS component, 2 | 'Welcome' AS title, 3 | 'Welcome, ' || $username || '! Your user account was successfully created. You can now log in.' AS description, 4 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image, 5 | 'signin.sql' AS link, 6 | 'Log in' AS link_text 7 | WHERE $error IS NULL; 8 | 9 | SELECT 'hero' AS component, 10 | 'Sorry' AS title, 11 | 'Sorry, the user name "' || $username || '" is already taken.' AS description, 12 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Sad_face_of_a_Wayuu_Woman.jpg/640px-Sad_face_of_a_Wayuu_Woman.jpg' AS image, 13 | 'signup.sql' AS link, 14 | 'Try again' AS link_text 15 | WHERE $error IS NOT NULL; -------------------------------------------------------------------------------- /examples/user-authentication/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version 4 | ports: 5 | - "8080:8080" 6 | volumes: 7 | - .:/var/www 8 | - ./sqlpage:/etc/sqlpage 9 | depends_on: 10 | - db 11 | environment: 12 | DATABASE_URL: postgres://root:secret@db/sqlpage 13 | db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases 14 | ports: 15 | - "5432:5432" 16 | - "3306:3306" 17 | image: postgres 18 | environment: 19 | POSTGRES_USER: root 20 | POSTGRES_DB: sqlpage 21 | POSTGRES_PASSWORD: secret 22 | -------------------------------------------------------------------------------- /examples/user-authentication/generate_password_hash.sql: -------------------------------------------------------------------------------- 1 | SELECT 'form' AS component; 2 | SELECT 'password' AS name, 'Password to create a hash for' AS label, :password AS value; 3 | 4 | SELECT 'code' AS component; 5 | SELECT sqlpage.hash_password(:password) AS contents; -------------------------------------------------------------------------------- /examples/user-authentication/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 2 | 'User Management App' AS title, 3 | 'user' AS icon, 4 | '/' AS link, 5 | CASE COALESCE(sqlpage.cookie('session'), '') 6 | WHEN '' THEN '["signin", "signup"]'::json 7 | ELSE '["logout"]'::json 8 | END AS menu_item; 9 | 10 | SELECT 'hero' AS component, 11 | 'SQLPage Authentication Demo' AS title, 12 | 'This application requires signing up to view the protected page.' AS description_md, 13 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Community_wp20.png/974px-Community_wp20.png' AS image, 14 | 'protected_page.sql' AS link, 15 | 'Access protected page' AS link_text; -------------------------------------------------------------------------------- /examples/user-authentication/login.sql: -------------------------------------------------------------------------------- 1 | -- The authentication component will stop the execution of the page and redirect the user to the login page if 2 | -- the password is incorrect or if the user does not exist. 3 | SELECT 'authentication' AS component, 4 | 'signin.sql?error' AS link, 5 | (SELECT password_hash FROM user_info WHERE username = :username) AS password_hash, 6 | :password AS password; 7 | 8 | -- Generate a random 32 characters session ID, insert it into the database, 9 | -- and save it in a cookie on the user's browser. 10 | INSERT INTO login_session (id, username) 11 | VALUES (sqlpage.random_string(32), :username) 12 | RETURNING 13 | 'cookie' AS component, 14 | 'session' AS name, 15 | id AS value, 16 | FALSE AS secure; -- You can remove this if the site is served over HTTPS. 17 | 18 | -- Redirect the user to the protected page. 19 | SELECT 'redirect' AS component, 'protected_page.sql' AS link; 20 | -------------------------------------------------------------------------------- /examples/user-authentication/logout.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM login_session WHERE id = sqlpage.cookie('session'); 2 | SELECT 'cookie' AS component, 'session' AS name, TRUE AS remove; 3 | 4 | SELECT 'redirect' AS component, '/' AS link; -------------------------------------------------------------------------------- /examples/user-authentication/protected_page.sql: -------------------------------------------------------------------------------- 1 | SET username = (SELECT username FROM login_session WHERE id = sqlpage.cookie('session')); 2 | 3 | SELECT 'redirect' AS component, 4 | 'signin.sql?error' AS link 5 | WHERE $username IS NULL; 6 | 7 | SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item; 8 | 9 | SELECT 'text' AS component, 10 | 'Welcome, ' || $username || ' !' AS title, 11 | 'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). 12 | You cannot view it if you are not connected.' AS contents_md; -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/duplicate-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/duplicate-user.png -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/homepage.png -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/secret.png -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/signin.png -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/signup-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/signup-success.png -------------------------------------------------------------------------------- /examples/user-authentication/screenshots/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/user-authentication/screenshots/signup.png -------------------------------------------------------------------------------- /examples/user-authentication/signin.sql: -------------------------------------------------------------------------------- 1 | SELECT 'form' AS component, 2 | 'Sign in' AS title, 3 | 'Sign in' AS validate, 4 | 'login.sql' AS action; 5 | 6 | SELECT 'username' AS name; 7 | SELECT 'password' AS name, 'password' AS type; 8 | 9 | SELECT 'alert' as component, 10 | 'Sorry' as title, 11 | 'We could not authenticate you. Please log in or [create an account](signup.sql).' as description_md, 12 | 'alert-circle' as icon, 13 | 'red' as color 14 | WHERE $error IS NOT NULL; -------------------------------------------------------------------------------- /examples/user-authentication/signup.sql: -------------------------------------------------------------------------------- 1 | SELECT 'form' AS component, 2 | 'Create a new user account' AS title, 3 | 'Sign up' AS validate, 4 | 'create_user.sql' AS action; 5 | 6 | SELECT 'username' AS name; 7 | SELECT 'password' AS name, 'password' AS type, '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$' AS pattern, 'Password must be at least 8 characters long and contain at least one letter and one number.' AS description; 8 | SELECT 'terms' AS name, 'I accept the terms and conditions' AS label, TRUE AS required, FALSE AS value, 'checkbox' AS type; -------------------------------------------------------------------------------- /examples/user-authentication/sqlpage/migrations/0000_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_info ( 2 | username TEXT PRIMARY KEY, 3 | password_hash TEXT NOT NULL 4 | ); 5 | 6 | CREATE TABLE login_session ( 7 | id TEXT PRIMARY KEY, 8 | username TEXT NOT NULL REFERENCES user_info(username), 9 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | -------------------------------------------------------------------------------- /examples/user-authentication/sqlpage/migrations/0001_create_admin_user.sql: -------------------------------------------------------------------------------- 1 | -- Creates an initial user called 'admin' 2 | -- with a password hash that was generated using the 'generate_password_hash.sql' page. 3 | INSERT INTO user_info (username, password_hash) 4 | VALUES ('admin', '$argon2id$v=19$m=19456,t=2,p=1$IiReWDP0ocWvia+fTdozJw$53EozOKX7HkpvOdoWHjsh9yKvRN2TmQm/PjYBeaOqqc'); -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/api.sql: -------------------------------------------------------------------------------- 1 | -- Adds a new entry in the clicks table 2 | INSERT INTO clicks(click_time) VALUES (datetime('now')); 3 | 4 | SELECT 'json' AS component, 5 | JSON_OBJECT( 6 | 'total_clicks', (SELECT count(*) FROM clicks) 7 | ) AS contents; -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/index.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 2 | 'SQLPage with a frontend component' as title, 3 | 'style.css' as css, 4 | 'settings' as icon, 5 | 'equations' as menu_item; 6 | 7 | SELECT 'button' AS component, 'center' as justify; 8 | SELECT 'Try my react component !' AS title, 'react.sql' AS link, 'funky_text' AS id; 9 | -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/react.sql: -------------------------------------------------------------------------------- 1 | SELECT 'shell' AS component, 2 | 'SQLPage with a frontend component' AS title, 3 | 'settings' AS icon, 4 | 5 | -- Including react from a CDN like that is quick and easy, but if your project grows larger, 6 | -- you might want to use a bundler like webpack, and include your javascript file here instead 7 | 'https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js' AS javascript, 8 | 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js' AS javascript, 9 | 'my_react_component.js' AS javascript; 10 | 11 | SELECT 'react_component' AS component, 12 | 'MyComponent' AS react_component_name, 13 | 'World' AS greeting_name; -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/screenshot-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/using react and other custom scripts and styles/screenshot-css.png -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/screenshot-latex-math-equations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/using react and other custom scripts and styles/screenshot-latex-math-equations.png -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/screenshot-math-equations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/using react and other custom scripts and styles/screenshot-math-equations.png -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/screenshot-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlpage/SQLPage/e90002a932ee497880fae320a8d3be807a50f315/examples/using react and other custom scripts and styles/screenshot-react.png -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/sqlpage/migrations/0001_clicks.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE clicks( 2 | click_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP 3 | ); -------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/sqlpage/templates/react_component.handlebars: -------------------------------------------------------------------------------- 1 |
2 | Dynamic component is loading... 3 |
-------------------------------------------------------------------------------- /examples/using react and other custom scripts and styles/style.css: -------------------------------------------------------------------------------- 1 | #funky_text { 2 | font-family: Arial, sans-serif; 3 | font-size: 36px; 4 | color: white; 5 | background: linear-gradient(45deg, #ff4e00, #ec9f05); 6 | padding: 10px; 7 | border-radius: 10px; 8 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); 9 | transition: transform 0.3s ease-in-out; 10 | } 11 | 12 | #funky_text:hover { 13 | transform: scale(1.1); 14 | background: linear-gradient(45deg, #e5004d, #7a0180); 15 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); 16 | } 17 | 18 | @keyframes neon-glow { 19 | 0%, 20 | 100% { 21 | text-shadow: 0 0 10px #fa2dd1, 0 0 20px #b30890, 2px 3px 30px #ff00cc; 22 | } 23 | 50% { 24 | text-shadow: 0 0 1px #e48fd3, 0 0 2px #ca28aa, 0 0 8px #ff00cc; 25 | } 26 | } 27 | 28 | #funky_text:hover { 29 | animation: neon-glow .5s ease-in-out infinite; 30 | } 31 | 32 | #funky_text * { 33 | color: inherit; 34 | } 35 | -------------------------------------------------------------------------------- /examples/web servers - apache/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sqlpage: 3 | image: lovasoa/sqlpage:main 4 | volumes: 5 | - ./sqlpage_config:/etc/sqlpage:ro 6 | - ./website:/var/www:ro 7 | environment: 8 | - DATABASE_URL=mysql://sqlpage:sqlpage_password@mysql:3306/sqlpage_db 9 | depends_on: 10 | - mysql 11 | 12 | apache: 13 | image: httpd:2.4 14 | ports: 15 | - "80:80" 16 | volumes: 17 | - ./apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro 18 | - ./static:/var/www:ro 19 | depends_on: 20 | - sqlpage 21 | 22 | mysql: 23 | image: mysql:8 24 | environment: 25 | - MYSQL_ROOT_PASSWORD=root_password 26 | - MYSQL_DATABASE=sqlpage_db 27 | - MYSQL_USER=sqlpage 28 | - MYSQL_PASSWORD=sqlpage_password 29 | volumes: 30 | - mysql_data:/var/lib/mysql 31 | 32 | volumes: 33 | mysql_data: -------------------------------------------------------------------------------- /examples/web servers - apache/sqlpage_config/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "site_prefix": "/my_website" 3 | } 4 | -------------------------------------------------------------------------------- /examples/web servers - apache/website/index.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'text' as component, 3 | true as article, 4 | ' 5 | # Welcome to my website 6 | 7 | Using SQLPage v' || sqlpage.version() || ' 8 | 9 | Connected to **MySQL** v' || version () as contents_md; 10 | -------------------------------------------------------------------------------- /lambda.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87-alpine as builder 2 | RUN rustup component add clippy rustfmt 3 | RUN apk add --no-cache musl-dev zip 4 | WORKDIR /usr/src/sqlpage 5 | RUN cargo init . 6 | COPY Cargo.toml Cargo.lock ./ 7 | RUN cargo build --release 8 | COPY . . 9 | RUN cargo build --release --features lambda-web 10 | RUN mv target/release/sqlpage bootstrap && \ 11 | strip --strip-all bootstrap && \ 12 | size bootstrap && \ 13 | ldd bootstrap && \ 14 | zip -9 -r deploy.zip bootstrap index.sql 15 | 16 | FROM public.ecr.aws/lambda/provided:al2 as runner 17 | COPY --from=builder /usr/src/sqlpage/bootstrap /main 18 | COPY --from=builder /usr/src/sqlpage/index.sql ./index.sql 19 | ENTRYPOINT ["/main"] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlpage", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "biome check .", 6 | "format": "biome format --write .", 7 | "fix": "biome check --fix --unsafe ." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/sqlpage/SQLPage.git" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@biomejs/biome": "^1.9.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sqlpage/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sqlpage/sqlpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "sqlite://./sqlpage/sqlpage.db?mode=rwc" 3 | } 4 | -------------------------------------------------------------------------------- /sqlpage/tabler-icons.svg: -------------------------------------------------------------------------------- 1 | /* !include https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@3.33.0/dist/tabler-sprite.svg */ 2 | -------------------------------------------------------------------------------- /sqlpage/templates/README.md: -------------------------------------------------------------------------------- 1 | # SQLPage component templates 2 | 3 | SQLPage templates are handlebars[^1] files that are used to render the results of SQL queries. 4 | 5 | [^1]: https://handlebarsjs.com/ 6 | 7 | ## Default components 8 | 9 | SQLPage comes with a set of default[^2] components that you can use without having to write any code. 10 | These are documented on https://sql-page.com/components.sql 11 | 12 | ## Custom components 13 | 14 | You can [write your own component templates](https://sql-page.com/custom_components.sql) 15 | and place them in the `sqlpage/templates` directory. 16 | To override a default component, create a file with the same name as the default component. 17 | If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory 18 | in the SQLPage source code[^2]. 19 | 20 | [^2]: A simple component to start from: https://github.com/sqlpage/SQLPage/blob/main/sqlpage/templates/code.handlebars -------------------------------------------------------------------------------- /sqlpage/templates/breadcrumb.handlebars: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /sqlpage/templates/code.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{#each_row}} 3 | {{#if title}}

{{title}}

{{/if}} 4 | {{#if description}}

{{description}}

{{else}}{{#if description_md}}{{{markdown description_md}}}{{/if}}{{/if}} 5 |
{{contents}}
6 | {{/each_row}} 7 |
8 | -------------------------------------------------------------------------------- /sqlpage/templates/debug.handlebars: -------------------------------------------------------------------------------- 1 |

Debug output

2 |
{{stringify this}}
3 | {{#each_row}}{{stringify this}}
4 | {{/each_row}}
5 | 
-------------------------------------------------------------------------------- /sqlpage/templates/default.handlebars: -------------------------------------------------------------------------------- 1 | {{#each_row}} 2 | {{#if tag}} 3 | <{{{tag}}}> 4 | {{~#each (entries this)~}} 5 | {{#unless (eq key "tag")}} 6 | {{~value~}} 7 | {{/unless}} 8 | {{~/each~}} 9 | 10 | {{else}} 11 |

12 | {{~#each (entries this)}} 13 | {{value}} 14 | {{/each~}} 15 |

16 | {{/if}} 17 | {{/each_row}} -------------------------------------------------------------------------------- /sqlpage/templates/divider.handlebars: -------------------------------------------------------------------------------- 1 | {{#if contents}} 2 |
12 | {{#if link}} 13 | 17 | {{contents}} 18 | 19 | {{else}} 20 | {{contents}} 21 | {{/if}} 22 |
23 | {{else}} 24 |
25 | {{/if}} 26 | -------------------------------------------------------------------------------- /sqlpage/templates/empty_state.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{#if header}} 3 |
{{header}}
4 | {{else}} 5 | {{#if icon}} 6 |
7 | {{icon_img icon }} 8 |
9 | {{else}} 10 | {{#if image}} 11 |
{{image}}
12 | {{/if}} 13 | {{/if}} 14 | {{/if}} 15 |

{{title}}

16 |
17 | {{~#if description}}

{{description}}

{{/if~}} 18 | {{~#if description_md~}} 19 | {{{markdown description_md}}} 20 | {{~/if~}} 21 |
22 | 23 | 29 |
30 | -------------------------------------------------------------------------------- /sqlpage/templates/html.handlebars: -------------------------------------------------------------------------------- 1 | {{{~html~}}} 2 | {{~#each_row~}} 3 | {{{~html~}}} 4 | {{~text~}} 5 | {{{~post_html~}}} 6 | {{~/each_row~}} 7 | -------------------------------------------------------------------------------- /sqlpage/templates/shell-empty.handlebars: -------------------------------------------------------------------------------- 1 | {{{~html~}}} 2 | {{~#each_row~}}{{~/each_row~}} 3 | -------------------------------------------------------------------------------- /sqlpage/templates/steps.handlebars: -------------------------------------------------------------------------------- 1 | {{#if id}}{{/if}} 2 | {{#if title}} 3 |

{{title}}

4 | {{/if}} 5 | {{#if description}} 6 |

{{description}}

7 | {{/if}} 8 |
25 | -------------------------------------------------------------------------------- /sqlpage/templates/title.handlebars: -------------------------------------------------------------------------------- 1 | {{contents}} 2 | -------------------------------------------------------------------------------- /src/webserver/request_variables.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{hash_map::Entry, HashMap}; 2 | 3 | use super::http::SingleOrVec; 4 | 5 | pub type ParamMap = HashMap; 6 | 7 | pub fn param_map>(values: PAIRS) -> ParamMap { 8 | values 9 | .into_iter() 10 | .fold(HashMap::new(), |mut map, (mut k, v)| { 11 | let entry = if k.ends_with("[]") { 12 | k.replace_range(k.len() - 2.., ""); 13 | SingleOrVec::Vec(vec![v]) 14 | } else { 15 | SingleOrVec::Single(v) 16 | }; 17 | match map.entry(k) { 18 | Entry::Occupied(mut s) => { 19 | SingleOrVec::merge(s.get_mut(), entry); 20 | } 21 | Entry::Vacant(v) => { 22 | v.insert(entry); 23 | } 24 | } 25 | map 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /tests/components/any_component.sql: -------------------------------------------------------------------------------- 1 | select $component as component; 2 | select 3 | 'It works !' as title, 4 | 'It works !' as description; 5 | 6 | select 'divider' as component, 'the end' as contents; -------------------------------------------------------------------------------- /tests/components/display_form_field.sql: -------------------------------------------------------------------------------- 1 | -- This test checks that the size of the form field can successfully roundtrip, 2 | -- from POST variable to sqlpage variable to handlebars, back to the client 3 | set x = :x; 4 | select 'text' as component, $x as contents; 5 | -------------------------------------------------------------------------------- /tests/components/display_text.sql: -------------------------------------------------------------------------------- 1 | select 'html' as component, $html as html; -------------------------------------------------------------------------------- /tests/components/mod.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{http::StatusCode, test}; 2 | use sqlpage::webserver::http::main_handler; 3 | 4 | use crate::common::get_request_to; 5 | 6 | #[actix_web::test] 7 | async fn test_overwrite_variable() -> actix_web::Result<()> { 8 | let req = get_request_to("/tests/sql_test_files/it_works_set_variable.sql") 9 | .await? 10 | .set_form(std::collections::HashMap::<&str, &str>::from_iter([( 11 | "what_does_it_do", 12 | "does not overwrite variables", 13 | )])) 14 | .to_srv_request(); 15 | let resp = main_handler(req).await?; 16 | 17 | assert_eq!(resp.status(), StatusCode::OK); 18 | let body = test::read_body(resp).await; 19 | let body_str = String::from_utf8(body.to_vec()).unwrap(); 20 | assert!( 21 | body_str.contains("It works !"), 22 | "{body_str}\nexpected to contain: It works !" 23 | ); 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /tests/core/.hidden.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 'This is a hidden file that should not be accessible' as contents; -------------------------------------------------------------------------------- /tests/core/select_temp_t.sql: -------------------------------------------------------------------------------- 1 | -- see tests/sql_test_files/it_works_temp_table_accessible_in_run_sql.sql 2 | select 'text' as component, x as contents from temp_t; -------------------------------------------------------------------------------- /tests/core/spaces in file name.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 'It works !' AS contents; -------------------------------------------------------------------------------- /tests/data_formats/csv_data.sql: -------------------------------------------------------------------------------- 1 | select 'csv' as component, ';' as separator; 2 | select 0 as id, 'Hello World !' as msg 3 | union all 4 | select 1 as id, 'Tu gères '';'' et ''"'' ?' as msg; -------------------------------------------------------------------------------- /tests/data_formats/json_columns.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'columns' as component; 3 | 4 | select 5 | 'Pro Plan' as title, 6 | '€40' as value, 7 | 'rocket' as icon, 8 | 'For growing projects needing enhanced features' as description, 9 | JSON ( 10 | '{"icon":"database","color":"blue","description":"1GB Database"}' 11 | ) as item, 12 | JSON ( 13 | '{"icon":"headset","color":"green","description":"Priority Support"}' 14 | ) as item; -------------------------------------------------------------------------------- /tests/data_formats/json_data.sql: -------------------------------------------------------------------------------- 1 | select 'json' as component; 2 | select 'It works!' as message; 3 | select 'cool' as cool; -------------------------------------------------------------------------------- /tests/end-to-end/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | -------------------------------------------------------------------------------- /tests/end-to-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "end-to-end", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "playwright test" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@playwright/test": "^1.45.3", 14 | "@types/node": "^22.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/errors/404.sql: -------------------------------------------------------------------------------- 1 | SELECT 'alert' AS component, 2 | 'We almost got an oopsie' AS title, 3 | 'But the `404.sql` file saved the day!' AS description_md; 4 | -------------------------------------------------------------------------------- /tests/it_works.txt: -------------------------------------------------------------------------------- 1 | It works ! -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod basic; 2 | mod common; 3 | mod components; 4 | mod core; 5 | mod data_formats; 6 | mod errors; 7 | mod requests; 8 | pub mod sql_test_files; 9 | mod transactions; 10 | mod uploads; 11 | -------------------------------------------------------------------------------- /tests/requests/request_body_base64_test.sql: -------------------------------------------------------------------------------- 1 | select 'shell-empty' as component, 2 | coalesce(sqlpage.request_body_base64(), 'NULL') as html; -------------------------------------------------------------------------------- /tests/requests/request_body_test.sql: -------------------------------------------------------------------------------- 1 | select 'shell-empty' as component, 2 | coalesce(sqlpage.request_body(), 'NULL') as html; -------------------------------------------------------------------------------- /tests/sql_test_files/README.md: -------------------------------------------------------------------------------- 1 | The sql files in this folder are all tested automatically. 2 | 3 | ## `it_works_` files 4 | 5 | Files with names starting with `it_works` should all 6 | return a page that contains the text "It works !" and does not contain the 7 | text "error" (case insensitive) when executed. 8 | 9 | If a file name contains `nosqlite`, `nomssql`, `nopostgres` or `nomysql`, then 10 | the test will be ignored when running against the corresponding database. 11 | This allows using syntax that is not supported on all databases in some tests. 12 | 13 | ## `error_` files 14 | 15 | Files with names starting with `error` should all return a page that contains 16 | the text "error" and the rest of the file name when executed. -------------------------------------------------------------------------------- /tests/sql_test_files/error_arbitrary_SQL_expressions_as_function_arguments_are_not_supported.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | lower(sqlpage.url_encode(lower('HELLO'))) as title; 3 | -- this is invalid, because the sqlpage pseudo-function is sandwiched between two native SQL functions. 4 | -- It can't be executed neither before nor after the query is executed. -------------------------------------------------------------------------------- /tests/sql_test_files/error_cookie_component_cannot_be_used_after_data_has_already_been_sent.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component; 2 | select 'hello world' as contents; 3 | select 'cookie' as component, 4 | 'username' as name, 5 | 'John Doe' as value; -------------------------------------------------------------------------------- /tests/sql_test_files/error_failed_to_import_the_csv.sql: -------------------------------------------------------------------------------- 1 | -- https://github.com/sqlpage/SQLPage/issues/788 2 | copy this_table_does_not_exist (csv) from 'recon_csv_file_input' DELIMITER '*' CSV; -------------------------------------------------------------------------------- /tests/sql_test_files/error_invalid_json_in_dynamic_component.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, 2 | '{ "this object": "has a forbidden comma" , }' as properties; -- this is invalid JSON, and should cause an error -------------------------------------------------------------------------------- /tests/sql_test_files/error_single_shell_per_page.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 'hello' as title; 2 | select 'shell' as component, 'world' as title; -------------------------------------------------------------------------------- /tests/sql_test_files/error_the_dynamic_component_requires_a_property_named.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component; -- missing "properties" column -------------------------------------------------------------------------------- /tests/sql_test_files/error_too_many_nested_inclusions.sql: -------------------------------------------------------------------------------- 1 | select 'debug' as component, 2 | sqlpage.run_sql( 3 | 'tests/sql_test_files/error_too_many_nested_inclusions.sql' 4 | ) as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/error_unknown_field.sql: -------------------------------------------------------------------------------- 1 | select sqlpage.fetch('{ 2 | "url": "https://example.com", 3 | "invalid_field": "should fail" 4 | }') -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_basic_auth_password.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.basic_auth_password() 3 | when 'test' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.basic_auth_password(), 'NULL') 5 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_basic_auth_username.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.basic_auth_username() 3 | when 'test' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.basic_auth_username(), 'NULL') 5 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_case_variables.sql: -------------------------------------------------------------------------------- 1 | -- https://github.com/sqlpage/SQLPage/issues/818 2 | 3 | set success = 'It works !'; 4 | set failure = 'You should never see this'; 5 | 6 | select 'text' as component, 7 | case $success 8 | when $success then $success 9 | when $failure then $failure 10 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_client_ip.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case when sqlpage.client_ip() is null then 'It works !' 3 | else 'It failed ! Got: ' || sqlpage.client_ip() 4 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_coalesce_eval.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.link(coalesce($i_do_not_exist, 'https://example.com')) 3 | when 'https://example.com' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.link(coalesce($i_do_not_exist, 'https://example.com')), 'NULL') 5 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_columns_component_json_nomssql_nopostgres.sql: -------------------------------------------------------------------------------- 1 | -- this test is skipped on MSSQL and postgres because their json_object function has a different syntax 2 | select 'columns' as component; 3 | select 4 | JSON_OBJECT('description', 'It works !') as item, 5 | JSON_OBJECT('description', 'It works !') as item 6 | ; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_concat_str_in_pseudofunction.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | 'With "||": ' || 3 | CASE sqlpage.url_encode('/' || $x) 4 | WHEN '%2F1' THEN 'It works !' 5 | ELSE 'Error: "/1" should be urlencoded to "%2F1"' 6 | END 7 | || ' | With CONCAT: ' || 8 | CASE sqlpage.url_encode(CONCAT('/', $x)) -- $x is set to '1' in the test 9 | WHEN '%2F1' THEN 'With CONCAT: It works !' 10 | ELSE 'Error: "/1" should be urlencoded to "%2F1"' 11 | END 12 | || ' | With a null value: ' || 13 | CASE COALESCE(sqlpage.url_encode(CONCAT('/', $thisisnull)), 'expected') 14 | WHEN 'expected' THEN 'With a null value: It works !' 15 | ELSE 'Error: a null value concatenated with "/" should be null, and urlencoded to NULL' 16 | END 17 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_cookie.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.cookie('test_cook') 3 | when '123' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.cookie('test_cook'), 'NULL') 5 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_create_table.sql: -------------------------------------------------------------------------------- 1 | drop table if exists my_tmp_store; 2 | create table my_tmp_store(x varchar(100)); 3 | 4 | insert into my_tmp_store(x) values ('It works !'); 5 | 6 | select 'card' as component; 7 | select x as description from my_tmp_store; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_decimal.sql: -------------------------------------------------------------------------------- 1 | set my_decimal = CAST(0.47 AS DECIMAL(3,2)); 2 | 3 | select 'text' as component, 4 | case $my_decimal 5 | when '0.47' then 'It works !' 6 | else 'error: ' || coalesce($my_decimal, 'NULL') 7 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_default_component.sql: -------------------------------------------------------------------------------- 1 | select 'yes' as "It works !"; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_delayed_function_call.sql: -------------------------------------------------------------------------------- 1 | drop table if exists files_to_read; 2 | create table files_to_read(filepath varchar(100)); 3 | insert into files_to_read(filepath) values ('tests/it_works.txt'); 4 | select 'text' as component, sqlpage.read_file_as_text(filepath) as contents from files_to_read; 5 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_dynamic_nested.sql: -------------------------------------------------------------------------------- 1 | -- Checks that we can have a page with a single dynamic component containing multiple children 2 | select 'dynamic' as component, 3 | '[ 4 | {"component":"dynamic", "properties": [ 5 | {"component":"text"}, 6 | {"contents":"It works !", "bold":true} 7 | ]} 8 | ]' as properties; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_dynamic_repeated_properties.sql: -------------------------------------------------------------------------------- 1 | -- Checks that we can have a page with a single dynamic component containing multiple children 2 | select 'dynamic' as component, 3 | '{"component":"text"}' as properties, 4 | '{"contents":"Hello, ", "bold":true}' as properties, 5 | '{"component":"text"}' as properties, 6 | '{"contents":"It works !", "bold":true}' as properties; 7 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_dynamic_run_sql_include.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('tests/sql_test_files/it_works_dynamic_shell.sql') as properties; 2 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_dynamic_shell.sql: -------------------------------------------------------------------------------- 1 | -- Checks that we can have a page with a single dynamic component containing multiple children 2 | select 'dynamic' as component, 3 | '[ 4 | {"component":"shell", "title":"It works !"}, 5 | {"component":"text"}, 6 | {"contents":"Yes it does !", "bold":true} 7 | ]' as properties; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_dynamic_toplevel.sql: -------------------------------------------------------------------------------- 1 | -- Checks that we can have a page with a single dynamic component containing multiple children 2 | select 'dynamic' as component, 3 | '[ 4 | {"component":"text"}, 5 | {"contents":"Hello, ", "bold":true}, 6 | {"component":"text"}, 7 | {"contents":"It works !", "bold":true} 8 | ]' as properties; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_env_var_empty.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, coalesce(sqlpage.environment_variable('I_DO_NOT_EXIST'), 'It works !') as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_exec.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, sqlpage.exec('echo', 'It', $thisisnull, 'works', '!') as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_fetch_native_json_array_impl.sql: -------------------------------------------------------------------------------- 1 | set url = 'http://localhost:' || $echo_port || '/post'; 2 | set res = sqlpage.fetch(json_object( 3 | 'method', 'POST', 4 | 'url', $url, 5 | 'headers', json_object('x-custom', '1'), 6 | 'body', json_array('hello', 'world') 7 | )); 8 | set expected = 'POST /post|accept-encoding: br, gzip, deflate, zstd|content-length: 17|content-type: application/json|host: localhost:' || $echo_port || '|user-agent: sqlpage|x-custom: 1|["hello","world"]'; 9 | select 'text' as component, 10 | case $res 11 | when $expected then 'It works !' 12 | else 'It failed ! Expected: 13 | ' || $expected || ' 14 | Got: 15 | ' || $res 16 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_fetch_post.sql: -------------------------------------------------------------------------------- 1 | set url = 'http://localhost:' || $echo_port || '/post'; 2 | set fetch_request = '{"method": "POST", "url": "' || $url || '", "headers": {"x-custom": "1"}, "body": {"hello": "world"}}'; 3 | set res = sqlpage.fetch($fetch_request); 4 | set expected = 'POST /post|accept-encoding: br, gzip, deflate, zstd|content-length: 18|content-type: application/json|host: localhost:' || $echo_port || '|user-agent: sqlpage|x-custom: 1|{"hello": "world"}'; 5 | select 'text' as component, 6 | case $res 7 | when $expected then 'It works !' 8 | else 'It failed ! Expected: 9 | ' || COALESCE($expected, 'null') || ' 10 | Got: 11 | ' || COALESCE($res, 'null') 12 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_fetch_simple.sql: -------------------------------------------------------------------------------- 1 | set url = 'http://localhost:' || $echo_port || '/hello_world'; 2 | set res = sqlpage.fetch($url); 3 | select 'text' as component, 4 | case 5 | when $res LIKE 'GET /hello_world%' then 'It works !' 6 | else 'It failed ! Got: ' || $res 7 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_fetch_with_meta_error.sql: -------------------------------------------------------------------------------- 1 | set res = sqlpage.fetch_with_meta('http://not-a-real-url'); 2 | 3 | select 'text' as component, 4 | case 5 | when $res LIKE '%"error":"Request failed%' then 'It works !' 6 | else CONCAT('Error! Got: ', $res) 7 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_fetch_with_meta_simple.sql: -------------------------------------------------------------------------------- 1 | set url = 'http://localhost:' || $echo_port || '/hello_world'; 2 | set fetch_req = '{ 3 | "method": "PUT", 4 | "url": "' || $url || '", 5 | "headers": { 6 | "user-agent": "myself" 7 | } 8 | }'; 9 | set res = sqlpage.fetch_with_meta($fetch_req); 10 | 11 | select 'text' as component, 12 | case 13 | when $res LIKE '%"status":200%' AND $res LIKE '%"headers":{%' AND $res LIKE '%"body":"%' then 'It works !' 14 | else 'Error! Got: ' || $res 15 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_hash_password.sql: -------------------------------------------------------------------------------- 1 | SELECT 'text' as component, 'It works ! The hashed password is: ' || sqlpage.hash_password($x) as contents; 2 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_hash_password_null.sql: -------------------------------------------------------------------------------- 1 | SELECT 'text' as component, 2 | case when sqlpage.hash_password(null) is null then 'It works !' else 'Error !' end 3 | as contents; 4 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_header.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.header('cookie') 3 | when 'test_cook=123' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.header('cookie'), 'NULL') 5 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_headers.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case when sqlpage.headers() LIKE '%"cookie":"test_cook=123"%' then 'It works !' 3 | else 'error: ' || sqlpage.headers() 4 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_link.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.link('test.sql', json_object('x', 123)) 3 | when 'test.sql?x=123' then 'It works !' 4 | else 'error: ' || coalesce(sqlpage.link('test.sql', json_object('x', 123)), 'NULL') 5 | end AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_lower.sql: -------------------------------------------------------------------------------- 1 | -- in SQLite, we provide our own unicode-aware lower function 2 | -- see https://github.com/sqlpage/SQLPage/issues/452 3 | select 'text' as component, COALESCE(lower(NULL), 'It works !') AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_markdown_in_table.sql: -------------------------------------------------------------------------------- 1 | -- https://github.com/sqlpage/SQLPage/discussions/600 2 | select 'table' as component, 'Link' AS markdown; -- uppercase Link 3 | select '[It works !](https://example.com) 4 | 5 | [comment]: <> (This is a comment. If this is visible, there is an error in markdown rendering)' as link; -- lowercase "link". 6 | -- If the markdown is not rendered, the page will contain the string "error" and trigger a test failure. 7 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_nested_sqlpage_functions.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case sqlpage.url_encode(sqlpage.read_file_as_text('tests/it_works.txt')) 3 | when 'It%20works%20%21' then 'It works !' 4 | else 'Error! Nested sqlpage functions are not working as expected.' 5 | end as contents; 6 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_path.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | CASE sqlpage.path() 3 | WHEN '/tests/sql_test_files/it_works_path.sql' THEN 'It works !' 4 | ELSE 'It failed ! Expected "/tests/sql_test_files/it_works_path.sql", got "' || sqlpage.path() || '"".' 5 | END 6 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_postgres_cast_syntax.sql: -------------------------------------------------------------------------------- 1 | -- the syntax $x::int is supported only in PostgreSQL 2 | -- but for consistency with other databases, sqlpage supports this syntax everywher () 3 | SELECT 'text' as component, 4 | case $x::decimal + 1 5 | when 2 then 'It works !' 6 | else 'Error !' 7 | end as contents; 8 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_protocol.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | CASE sqlpage.protocol() 3 | WHEN 'http' THEN 'It works !' 4 | ELSE 'It failed ! Expected "http", got "' || sqlpage.protocol() || '"".' 5 | END 6 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_query_string.sql: -------------------------------------------------------------------------------- 1 | set actual = sqlpage.link('', sqlpage.variables('get')); 2 | set expected = '?x=1'; 3 | SELECT 4 | 'text' AS component, 5 | CASE $actual 6 | WHEN $expected THEN 7 | 'It works !' 8 | ELSE 9 | 'Expected ' || COALESCE($expected, 'null') || ' but got ' || COALESCE($actual, 'null') 10 | END AS contents 11 | ; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_random_string.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | case when sqlpage.random_string(0) = '' -- with 0 as a parameter, the function becomes deterministic ;) 3 | then 'It works !' 4 | else 'Error ! sqlpage.random_string(5) = ' || sqlpage.random_string(5) 5 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_read_file_as_data_url.sql: -------------------------------------------------------------------------------- 1 | set actual = sqlpage.read_file_as_data_url('tests/it_works.txt') 2 | set expected = 'data:text/plain;base64,SXQgd29ya3MgIQ=='; 3 | 4 | select 'text' as component, 5 | case $actual 6 | when $expected 7 | then 'It works !' 8 | else 9 | 'Failed. 10 | Expected: ' || $expected || 11 | 'Got: ' || $actual 12 | end as contents; 13 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_read_file_as_text.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, sqlpage.read_file_as_text('tests/it_works.txt') as contents; 2 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_request_method.sql: -------------------------------------------------------------------------------- 1 | set actual = sqlpage.request_method() 2 | set expected = 'GET'; 3 | 4 | select 'text' as component, 5 | case $actual 6 | when $expected 7 | then 'It works !' 8 | else 9 | 'Failed. 10 | Expected: ' || $expected || 11 | 'Got: ' || $actual 12 | end as contents; 13 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_run_sql.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('tests/sql_test_files/it_works_simple.sql') as properties; 2 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_run_sql_from_database.sql: -------------------------------------------------------------------------------- 1 | select 2 | 'dynamic' as component, 3 | sqlpage.run_sql ( 4 | 'tests/components/display_text.sql', 5 | CONCAT ('{"html":"', html, '"}') -- UNSAFE. Don't do that with untrusted data. We do it here because we can't use json_object (the syntax is not the same in all supported databases) 6 | ) as properties 7 | from 8 | ( 9 | select 10 | 'It ' as html 11 | union all 12 | select 13 | 'works !' 14 | ) as t1; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_run_sql_variable_access.sql: -------------------------------------------------------------------------------- 1 | set html = 'It '; 2 | select 'dynamic' as component, sqlpage.run_sql('tests/components/display_text.sql') as properties; 3 | set html = 'works !'; 4 | select 'dynamic' as component, sqlpage.run_sql('tests/components/display_text.sql') as properties; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_set_variable.sql: -------------------------------------------------------------------------------- 1 | set what_does_it_do = 'wo' || 'rks'; 2 | select 'text' as component, 'It ' || $what_does_it_do || ' !' as contents; 3 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_set_variable_numeric.sql: -------------------------------------------------------------------------------- 1 | set two = 2; 2 | select 'text' as component, 3 | CASE 4 | WHEN $two = '2' -- All variables are strings 5 | THEN 6 | 'It works !' 7 | ELSE 8 | 'error: expected "2", got: ' || COALESCE($two, 'null') 9 | END as contents; 10 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_set_variable_to_null.sql: -------------------------------------------------------------------------------- 1 | set i_am_null = NULL; 2 | select 'text' as component, 3 | CASE 4 | WHEN $i_am_null IS NULL 5 | THEN 6 | 'It works !' 7 | ELSE 8 | 'error: expected null, got: ' || $i_am_null 9 | END as contents; 10 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_set_variable_to_sqlpage_function.sql: -------------------------------------------------------------------------------- 1 | set my_var = sqlpage.url_encode(' '); 2 | select 'text' as component, 3 | CASE $my_var 4 | WHEN '%20' THEN 'It works !' 5 | ELSE 'It failed !' 6 | END 7 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_set_variable_to_sqlpage_post_function.sql: -------------------------------------------------------------------------------- 1 | set my_var = sqlpage.url_encode(UPPER('a')); 2 | select 'text' as component, 3 | CASE $my_var 4 | WHEN 'A' THEN 'It works !' 5 | ELSE 'It failed !' 6 | END 7 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_shell_search.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 2 | '/' as search_target, 3 | 'It works !' as search_value; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_simple.sql: -------------------------------------------------------------------------------- 1 | select 'shell' as component, 'Hello world !' AS title; 2 | select 'text' as component, 'It works !' AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_sqlite_unicode_upper.sql: -------------------------------------------------------------------------------- 1 | -- Checks that the UPPER function is working correctly with unicode characters. 2 | select 'text' as component, 3 | case 4 | when UPPER('é') = 'É' then 'It works !' 5 | else 'It failed ! Expected É, got ' || UPPER('é') || '.' 6 | end as contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_temp_table_accessible_in_run_sql_nomssql.sql: -------------------------------------------------------------------------------- 1 | -- Doesnt work on mssql because it does not support "create temporary table" 2 | create temporary table temp_t(x text); 3 | insert into temp_t(x) values ('It works !'); 4 | select 'dynamic' as component, sqlpage.run_sql('tests/sql_test_files/select_temp_t.sql') AS properties; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_text_markdown.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | '### It works !' AS contents_md; 3 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_text_unsafe_markdown.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | 'It works !' AS unsafe_contents_md; 3 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_uploaded_file_is_null.sql: -------------------------------------------------------------------------------- 1 | -- checks that sqlpage.uploaded_file_path returns null when there is no uploaded_file 2 | set actual = sqlpage.uploaded_file_path('my_file'); 3 | select 'text' as component, 4 | case when $actual is null 5 | then 'It works !' 6 | else 'Failed. Expected: null. Got: ' || $actual 7 | end as contents; 8 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_url_encode.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | CASE sqlpage.url_encode('/') 3 | WHEN '%2F' THEN 'It works !' 4 | ELSE 'It failed !' 5 | END 6 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_varchar_nomysql.sql: -------------------------------------------------------------------------------- 1 | -- syntax is valid in SQLite, PostgreSQL and SQLServer 2 | -- no cast as varchar in MySQL 3 | select 'text' as component, CAST('It works !' as varchar(100)) as contents; 4 | -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_variables_function.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | CASE sqlpage.variables('get') 3 | WHEN '{"x":"1"}' THEN 'It works !' 4 | ELSE 'It failed ! Expected {"x":"1"}, got ' || sqlpage.variables('get') 5 | END 6 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_variables_function_get_and_post.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | CASE sqlpage.variables() 3 | WHEN '{"x":"1"}' THEN 'It works !' 4 | ELSE 'It failed ! Expected {"x":"1"}, got ' || sqlpage.variables() 5 | END 6 | AS contents; -------------------------------------------------------------------------------- /tests/sql_test_files/it_works_without_a_shell.sql: -------------------------------------------------------------------------------- 1 | select 'shell-empty' as component; 2 | select 'text' as component, 'My page

It works !

' as html; -------------------------------------------------------------------------------- /tests/sql_test_files/select_temp_t.sql: -------------------------------------------------------------------------------- 1 | -- see tests/sql_test_files/it_works_temp_table_accessible_in_run_sql.sql 2 | select 'text' as component, x as contents from temp_t; -------------------------------------------------------------------------------- /tests/transactions/failed_transaction.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | CREATE TEMPORARY TABLE t(f VARCHAR(100) NOT NULL); 3 | INSERT INTO t(f) VALUES ($x); 4 | COMMIT; 5 | 6 | select 'text' as component, f as contents from t; -------------------------------------------------------------------------------- /tests/transactions/failed_transaction_mssql.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | CREATE TABLE #t(f VARCHAR(100) NOT NULL); 3 | INSERT INTO #t(f) VALUES ($x); 4 | COMMIT TRANSACTION; 5 | 6 | SELECT 'text' AS component, f AS contents FROM #t; -------------------------------------------------------------------------------- /tests/transactions/failed_transaction_mysql.sql: -------------------------------------------------------------------------------- 1 | START TRANSACTION; -- mysql syntax 2 | CREATE TEMPORARY TABLE IF NOT EXISTS t(f VARCHAR(255) NOT NULL); -- mysql does not remove temporary tables on rollback 3 | INSERT INTO t(f) SELECT 'bad value' where $x is null; 4 | SELECT 'debug' as component, f from t; 5 | INSERT INTO t(f) VALUES ($x); -- this will fail if $x is null. And the transaction should be rolled back, so 'bad value' should not be in the table 6 | COMMIT; 7 | 8 | select 'text' as component, $x as contents 9 | where not exists (select 1 from t where f = 'bad value'); 10 | 11 | -------------------------------------------------------------------------------- /tests/uploads/upload_csv_test.sql: -------------------------------------------------------------------------------- 1 | drop table if exists sqlpage_people_test_table; 2 | create table sqlpage_people_test_table(name text, age text); 3 | copy sqlpage_people_test_table(name, age) from 'people_file' with (format csv, header true); 4 | select 'text' as component, 5 | name || ' is ' || age || ' years old. ' as contents 6 | from sqlpage_people_test_table; -------------------------------------------------------------------------------- /tests/uploads/upload_file_data_url_test.sql: -------------------------------------------------------------------------------- 1 | -- display the data url of the uploaded file, unencoded, and nothing else 2 | select 'shell-empty' as component, 3 | sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path('my_file')) as html; -------------------------------------------------------------------------------- /tests/uploads/upload_file_runsql_test.sql: -------------------------------------------------------------------------------- 1 | select 'dynamic' as component, sqlpage.run_sql('tests/uploads/upload_file_test.sql') as properties; -------------------------------------------------------------------------------- /tests/uploads/upload_file_test.sql: -------------------------------------------------------------------------------- 1 | select 'text' as component, 2 | COALESCE( 3 | sqlpage.read_file_as_text(sqlpage.uploaded_file_path('my_file')), 4 | 'No file uploaded' 5 | ) as contents; -------------------------------------------------------------------------------- /tests/uploads/uploaded_file_name_test.sql: -------------------------------------------------------------------------------- 1 | -- display the file name of the uploaded file, unencoded, and nothing else 2 | select 'shell-empty' as component, 3 | sqlpage.uploaded_file_name('my_file') as html; 4 | --------------------------------------------------------------------------------