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
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:
@@ -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;
|
||||
+20
@@ -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;
|
||||
Reference in New Issue
Block a user