chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
@@ -0,0 +1,5 @@
# Website editor
SQLPage supports rendering `.sql` files that are stored directly inside the database, in a table called `sqlpage_files`.
This application allows you to edit these files directly from your browser, for an easy in-browser data app authoring experience.
@@ -0,0 +1,18 @@
services:
web:
image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version
ports:
- "8080:8080"
volumes:
- ./website:/var/www
- ./sqlpage:/etc/sqlpage
depends_on:
- db
environment:
DATABASE_URL: postgres://root:secret@db/sqlpage
db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases
image: postgres
environment:
POSTGRES_USER: root
POSTGRES_DB: sqlpage
POSTGRES_PASSWORD: secret
@@ -0,0 +1,10 @@
-- Given a table name as text, return the contents of the table as a set of json objects
-- Safely escapes the table name to prevent SQL injection.
-- Accepts only normal tables, not postgres system tables.
CREATE OR REPLACE FUNCTION table_contents (table_name text)
RETURNS SETOF json AS $$
BEGIN
RETURN QUERY EXECUTE
format('SELECT row_to_json(%I) FROM %I', table_name, table_name);
END;
$$ LANGUAGE plpgsql;
@@ -0,0 +1,20 @@
CREATE TABLE
sqlpage_files (
path VARCHAR(255) NOT NULL PRIMARY KEY,
contents BYTEA NOT NULL,
last_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- automatically update last_modified timestamp
CREATE OR REPLACE FUNCTION update_last_modified_sqlpage_files()
RETURNS TRIGGER AS $$
BEGIN
NEW.last_modified = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_last_modified BEFORE
UPDATE ON sqlpage_files FOR EACH ROW
EXECUTE PROCEDURE update_last_modified_sqlpage_files();
@@ -0,0 +1,3 @@
{
"content_security_policy": "script-src blob: 'self' https://cdn.jsdelivr.net;"
}
@@ -0,0 +1,65 @@
GET http://localhost:8080/
HTTP 200
[Asserts]
header "Content-Type" contains "text/html"
body contains "Website files"
body contains "Create new file"
body contains "Database tables"
body contains "sqlpage_files"
body htmlUnescape contains "view_table.sql?table%5Fname=sqlpage%5Ffiles"
body not contains "An error occurred"
GET http://localhost:8080/edit.sql
HTTP 200
[Asserts]
body contains "New page"
body contains "insert_file.sql"
body contains "code-editor"
body not contains "An error occurred"
POST http://localhost:8080/insert_file.sql
[FormParams]
path: hurl_test.sql
contents: SELECT 'text' as component, 'Hello from Hurl' as contents;
HTTP 302
[Asserts]
header "Location" == "index.sql?inserted=hurl%5Ftest%2Esql"
GET http://localhost:8080/hurl_test.sql
HTTP 200
[Asserts]
body contains "Hello from Hurl"
body not contains "An error occurred"
GET http://localhost:8080/
HTTP 200
[Asserts]
body contains "hurl_test.sql"
body htmlUnescape contains "edit.sql?path=hurl%5Ftest%2Esql"
body htmlUnescape contains "delete.sql?path=hurl%5Ftest%2Esql"
body not contains "An error occurred"
GET http://localhost:8080/view_table.sql?table_name=sqlpage_files
HTTP 200
[Asserts]
body contains "sqlpage_files"
body contains "hurl_test.sql"
body not contains "An error occurred"
GET http://localhost:8080/delete.sql?path=hurl_test.sql
HTTP 200
[Asserts]
body contains "Delete hurl_test.sql ?"
body htmlUnescape contains "delete.sql?path=hurl%5Ftest%2Esql&confirm=yes"
body not contains "An error occurred"
GET http://localhost:8080/delete.sql?path=hurl_test.sql&confirm=yes
HTTP 302
[Asserts]
header "Location" == "index.sql?deleted=hurl%5Ftest%2Esql"
GET http://localhost:8080/
HTTP 200
[Asserts]
body not contains "hurl_test.sql"
body not contains "An error occurred"
@@ -0,0 +1,15 @@
delete from sqlpage_files
where path = $path and $confirm = 'yes'
returning
'redirect' as component,
sqlpage.link(
'index.sql',
json_build_object('deleted', $path)
) as link;
select 'alert' as component,
'Delete ' || $path || ' ?' as title,
'Are you sure you want to delete ' || $path || '?' as description,
'warning' as color,
sqlpage.link('delete.sql', json_build_object('path', $path, 'confirm', 'yes')) as link,
'Delete' as link_text;
@@ -0,0 +1,23 @@
select 'shell' as component,
'js/code-editor.js' as javascript;
select
'form' as component,
COALESCE('Editing ' || $path, 'New page') as title,
'insert_file.sql' as action;
select
'path' as name,
'text' as type,
'Name' as label,
$path as value,
'test.sql' as placeholder;
select
'textarea' as type,
'contents' as name,
'code-editor' as id,
'Contents' as label,
(select contents from sqlpage_files where path = $path) as value,
'SELECT ''text'' as component,
''Hello, world!'' as contents;' as placeholder;
@@ -0,0 +1,29 @@
select
'list' as component,
'Website files' as title;
select
path as title,
path as link,
sqlpage.link ('edit.sql', json_build_object ('path', path)) as edit_link,
sqlpage.link ('delete.sql', json_build_object ('path', path)) as delete_link
from
sqlpage_files;
select
'Create new file' as title,
'edit.sql' as link,
'file-plus' as icon,
'green' as color;
select 'list' as component,
'Database tables' as title;
select
table_name as title,
sqlpage.link ('view_table.sql', json_build_object('table_name', table_name)) as link
from
information_schema.tables
where
table_schema = 'public'
and table_type = 'BASE TABLE';
@@ -0,0 +1,10 @@
insert into sqlpage_files (path, contents)
values (:path, :contents::bytea)
on conflict (path)
do update set contents = excluded.contents
returning
'redirect' as component,
sqlpage.link(
'index.sql',
json_build_object('inserted', :path)
) as link;
@@ -0,0 +1,34 @@
const cdn = "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/";
function on_monaco_load() {
// Create an editor div, display it after the '#code-editor' textarea, hide the textarea, and create a Monaco editor in the div with the contents of the textarea
// When the form is submitted, set the value of the textarea to the value of the Monaco editor
const textarea = document.getElementById("code-editor");
const editorDiv = document.createElement("div");
editorDiv.style.width = "100%";
editorDiv.style.height = "700px";
textarea.parentNode.insertBefore(editorDiv, textarea.nextSibling);
const monacoConfig = {
value: textarea.value,
language: "sql",
};
self.MonacoEnvironment = {
baseUrl: `${cdn}min/`,
};
const editor = monaco.editor.create(editorDiv, monacoConfig);
textarea.style.display = "none";
const form = textarea.form;
form.addEventListener("submit", () => {
textarea.value = editor.getValue();
});
}
function set_require_config() {
require.config({ paths: { vs: `${cdn}min/vs` } });
require(["vs/editor/editor.main"], on_monaco_load);
}
const loader_script = document.createElement("script");
loader_script.src = `${cdn}min/vs/loader.js`;
loader_script.onload = set_require_config;
document.head.appendChild(loader_script);
@@ -0,0 +1,15 @@
select 'title' as component, $table_name as contents;
select 'table' as component, true as search, true as sort;
select 'dynamic' as component, t as properties
from table_contents($table_name) t
LIMIT 1000;
select 'alert' as component,
CASE
WHEN COUNT(*) = 0 THEN 'The table is empty.'
WHEN COUNT(*) > 1000 THEN 'Only the first 1000 rows are shown.'
END as description,
'info' as color
from table_contents($table_name)
HAVING NOT COUNT(*) BETWEEN 1 AND 1000;