Skip to content

Commit

Permalink
Add tables for configuration and the Servers Object
Browse files Browse the repository at this point in the history
In configuration.sql:
 - Added a generic configuration table
 - Added a servers table for configuring the Servers block
 - Added openapi_server_objects which returns JSON based on the servers table
 - Added set_server_from_configuration, which updates the servers table based on the configuration values
In main.sql:
 - Added callable_root function which can be directly called by PostgREST
 - Added x-software block, for specifying software versions
In fixtures.sql:
 - For testing, added a pre_config function that sets the pgrst.server_host and pgrst.server_port
Tests:
 - Added tests for servers and main
  • Loading branch information
wayland authored Nov 11, 2023
1 parent 81a6c08 commit 997735a
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 40 deletions.
22 changes: 18 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ clean_fixtures:
# extra dep for PostgreSQL targets in pgxs.mk
clean: clean_fixtures

# Docker stuff
##### Docker stuff
PWD := $(shell pwd)
BASE_VAR := $(shell basename $(PWD))
CONTAINER_NAME := $(BASE_VAR)_postgrest-openapi-build_1
DOCKER_COMPOSE_COMMAND_BASE=docker-compose --project-directory . --env-file hosting/environment.env
DOCKER_COMPOSE_COMMAND_TESTS=$(DOCKER_COMPOSE_COMMAND_BASE) --file hosting/tests/docker-compose.yml
docker-build-test:
$(DOCKER_COMPOSE_COMMAND_TESTS) build --force
$(DOCKER_COMPOSE_COMMAND_TESTS) down --remove-orphans
$(DOCKER_COMPOSE_COMMAND_TESTS) up -d
sleep 3
docker logs postgrest-openapi_postgrest-openapi-build_1
sleep 4
docker logs $(CONTAINER_NAME)

DOCKER_COMPOSE_COMMAND_FINAL=$(DOCKER_COMPOSE_COMMAND_BASE) --file hosting/final/docker-compose.yml
docker-build: docker-build-test
Expand All @@ -48,7 +51,18 @@ docker-build-and-run: docker-build
$(DOCKER_COMPOSE_COMMAND_BAR) down --remove-orphans
$(DOCKER_COMPOSE_COMMAND_BAR) up -d

# Postgres stuff
# make docker-output-save EXPORT=main
docker-output-save: docker-build-test
ifeq ("$(EXPORT)","")
echo "Please use: make docker-output-save EXPORT=<test-set>\nWhere <test-set> is any of the files in test/sql (but without the sql on the end"
else
$(eval CONTAINER_ID := $(shell docker inspect --type container --format '{{.Id}}' $(CONTAINER_NAME)) )
docker commit $(CONTAINER_ID) debug/$(CONTAINER_NAME)
docker run -it --rm --entrypoint sh debug/$(CONTAINER_NAME) -c "cat /buildroot/output/results/$(EXPORT).out" | dos2unix > test/expected/$(EXPORT).out
endif


##### Postgres stuff
TESTS = $(wildcard test/sql/*.sql)
REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS))
REGRESS_OPTS = --use-existing --inputdir=test --outputdir=output
Expand Down
38 changes: 32 additions & 6 deletions sql/main.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
-- Default PostgREST OpenAPI Specification

-- This is the one we should be calling
create or replace function callable_root() returns jsonb as $$
-- Calling this every time is inefficient, but it's the best we can do until PostgREST calls it when it updates the server config
CALL set_server_from_configuration();
SELECT get_postgrest_openapi_spec(
schemas := string_to_array(current_setting('pgrst.db_schemas', TRUE), ','),
postgrest_version := current_setting('pgrst.version', TRUE),
proa_version := '0.1'::text, -- TODO: needs to be updated; put into config, and have Makefile update
document_version := 'unset'::text
);
$$ language sql;

-- This one returns the OpenAPI JSON; instead of calling it directly, call "callable_root", below
create or replace function get_postgrest_openapi_spec(
schemas text[],
server_proxy_uri text default 'http://0.0.0.0:3000',
version text default null
postgrest_version text default null,
proa_version text default null,
document_version text default null
)
returns jsonb language sql as
$$
Expand All @@ -12,17 +26,29 @@ select openapi_object(
info := openapi_info_object(
title := coalesce(sd.title, 'PostgREST API'),
description := coalesce(sd.description, 'This is a dynamic API generated by PostgREST'),
version := coalesce(version, postgrest_get_version())
-- The document version
version := coalesce(document_version, 'undefined')
),
servers := jsonb_build_array(
openapi_server_object(
url := server_proxy_uri
xsoftware := jsonb_build_array(
-- The version of the OpenAPI extension
openapi_x_software_object(
name := 'OpenAPI',
version := proa_version,
description := 'Automatically/dynamically generate an OpenAPI schema for the API generated by PostgREST'
),
-- The version of PostgREST
openapi_x_software_object(
name := 'PostgREST API',
version := postgrest_version,
description := 'Automatically/dynamically turns a PostgreSQL database directly into a RESTful API'
)
),
servers := openapi_server_objects(),
paths := '{}',
components := openapi_components_object(
schemas := postgrest_tables_to_openapi_schema_components(schemas) || postgrest_composite_types_to_openapi_schema_components(schemas)
)
)
from postgrest_get_schema_description(schemas[1]) sd;
$$;

30 changes: 16 additions & 14 deletions sql/openapi.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
create or replace function openapi_object(
openapi text,
info jsonb,
xsoftware jsonb,
paths jsonb,
jsonSchemaDialect text default null,
servers jsonb default null,
Expand All @@ -18,6 +19,7 @@ select jsonb_strip_nulls(
jsonb_build_object(
'openapi', openapi,
'info', info,
'x-software', xsoftware,
'paths', paths,
'jsonSchemaDialect', jsonSchemaDialect,
'servers', servers,
Expand All @@ -30,20 +32,6 @@ select jsonb_strip_nulls(
);
$$;

create or replace function openapi_server_object(
url text,
description text default null,
variables jsonb default null
)
returns jsonb language sql as
$$
select jsonb_build_object(
'url', url,
'description', description,
'variables', variables
)
$$;

create or replace function openapi_info_object(
title text,
version text,
Expand All @@ -66,6 +54,20 @@ select jsonb_build_object(
);
$$;

create or replace function openapi_x_software_object(
name text,
version text,
description text
)
returns jsonb language sql as
$$
select json_build_object(
'x-name', name,
'x-version', version,
'x-description', description
);
$$;

create or replace function openapi_components_object(
schemas jsonb default null,
responses jsonb default null,
Expand Down
122 changes: 122 additions & 0 deletions sql/servers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
File: servers.sql
Purpose: Builds/manages the `servers` section of the OpenAPI document
Other doco/notes:
- Things to search for in this file: TODO, MVP
*/

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION openapi" to load this file. \quit

---------- General Configuration (excluding servers) ----------
-- Should probably be removed when we have config stuff working properly
CREATE TABLE configuration (
name TEXT,
value TEXT,
PRIMARY KEY (name)
);
-- Would've liked to have copied from STDIN, but that doesn't seem to be available in extensions
INSERT INTO configuration (name, value) VALUES ('mode', 'follow-privileges');
INSERT INTO configuration (name, value) VALUES ('security-active', 'false');
INSERT INTO configuration (name, value) VALUES ('api-name', '');
INSERT INTO configuration (name, value) VALUES ('api-version', '');
INSERT INTO configuration (name, value) VALUES ('api-description', '');
SELECT pg_catalog.pg_extension_config_dump('configuration', '');
-- openapi-server-proxy-uri has been replaced with the Servers configuration below

-- TODO: (MVP) Can we store the above stuff in the postgres set_config -type setup? The set_config function only applies for the current session



---------- Servers Configuration and Fetching ----------
/*
Specification: https://spec.openapis.org/oas/v3.0.1#server-object
Has a few additions to support server overriding:
- Each server can be overridden
- Servers with the same slug override each other; servers with different slugs are completely unrelated
- Overriding is controlled by the priority; the higher the number, the higher the priority
- Priority 10 is reserved for default values
- Priority 20 is reserved for values copied from configuration
*/
-- Configuration table, including defaults
CREATE TABLE servers (
slug TEXT NOT NULL DEFAULT 'default', -- A slug that says what category of URL we're looking at
url TEXT NOT NULL, -- URL as per OpenAPI 3.0.1
-- TODO: would like to convert to https://github.com/petere/pguri but not sure what the team will think -- see question at https://github.com/PostgREST/postgrest/issues/1698
description TEXT,
variables JSONB,
priority INTEGER NOT NULL,
PRIMARY KEY (slug, priority)
);
INSERT INTO servers (url, description, priority) VALUES ('http://0.0.0.0:3000/', 'Default URL', 10);
-- That second one will be replaced with the value from the configuration
SELECT pg_catalog.pg_extension_config_dump('servers', '');

-- Get servers, with the priorities, etc, already sorted out
CREATE OR REPLACE FUNCTION openapi_server_objects()
RETURNS JSONB AS $$
DECLARE
json_result jsonb;
BEGIN
-- cf. https://stackoverflow.com/questions/61874745/postgresql-get-first-non-null-value-per-group
WITH
tservers AS (
SELECT slug, url, description, variables FROM servers ORDER BY priority DESC
),
jq AS (SELECT
(ARRAY_AGG(url ) FILTER (WHERE url IS NOT NULL))[1] AS url,
(ARRAY_AGG(description) FILTER (WHERE description IS NOT NULL))[1] AS description,
(ARRAY_AGG(variables ) FILTER (WHERE variables IS NOT NULL))[1] AS variables -- URL variables
FROM tservers
GROUP BY slug
ORDER BY slug
)
SELECT json_agg(jq) FROM jq INTO json_result;

RETURN json_result;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE PROCEDURE set_server_from_configuration()
AS $$
DECLARE
scheme text;
host text;
port int;

url_string text;
description_string text;
BEGIN
port:= COALESCE(NULLIF(REGEXP_REPLACE(current_setting('pgrst.server_port', TRUE), '[^0-9]', '', 'g'), '')::bigint, 443);
scheme := CASE
WHEN port = 80 THEN 'http'
ELSE 'https'
END;
host := CASE
WHEN current_setting('pgrst.server_host', TRUE) ~ '^(\*[46]?|\![46])$' THEN '0.0.0.0'
ELSE current_setting('pgrst.server_host', TRUE)
END;

IF LENGTH(host) > 0 AND port > 0 AND LENGTH(scheme) > 0 THEN
url_string := FORMAT('%s://%s:%s/', scheme, host, port);
description_string := 'URL from configuration';
INSERT INTO servers (url, description, slug, priority)
VALUES (url_string, description_string, 'default', 20)
ON CONFLICT (slug, priority) DO
UPDATE SET
url = url_string,
description = description_string;
ELSE
DELETE FROM servers WHERE
slug = 'default'
AND priority = 20;
END IF;
END;
$$ LANGUAGE plpgsql;

-- Calls this config on startup
-- TODO: (MVP) How do we run this on config change?
CALL set_server_from_configuration();
41 changes: 41 additions & 0 deletions test/expected/main.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Loads the config -- only needed because we're not using actual PostgREST in the tests
SELECT postgrest.pre_config();
pre_config
------------

(1 row)

-- Sets the config; would be called if we called callable_root() (in main)
CALL set_server_from_configuration();
-- Ensures the config is loaded
SELECT current_setting('pgrst.db_schemas', TRUE);
current_setting
-----------------
test
(1 row)

SELECT string_to_array(current_setting('pgrst.db_schemas', TRUE), ',');
string_to_array
-----------------
{test}
(1 row)

-- Tests get_postgrest_openapi_spec with parameters
SELECT count(get_postgrest_openapi_spec(
schemas := string_to_array(current_setting('pgrst.db_schemas', TRUE), ','),
postgrest_version := current_setting('pgrst.version', TRUE),
proa_version := '0.1'::text, -- TODO: needs to be updated; put into config, and have Makefile update
document_version := 'unset'::text
));
count
-------
1
(1 row)

-- shows the default server
SELECT count(jsonb_pretty(callable_root()));
count
-------
1
(1 row)

Loading

0 comments on commit 997735a

Please sign in to comment.