Add TypeScript definitions via pg-to-ts and refactor migrations

This commit is contained in:
thiloho
2024-09-10 17:29:57 +02:00
parent 8121be1d96
commit c5fbcdc8bd
50 changed files with 1525 additions and 1632 deletions

View File

@@ -1,21 +1,25 @@
-- migrate:up -- migrate:up
CREATE SCHEMA internal;
CREATE SCHEMA api; CREATE SCHEMA api;
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
CREATE ROLE anon NOLOGIN NOINHERIT; CREATE ROLE anon NOLOGIN NOINHERIT;
GRANT USAGE ON SCHEMA api TO anon;
CREATE ROLE authenticated_user NOLOGIN NOINHERIT; CREATE ROLE authenticated_user NOLOGIN NOINHERIT;
GRANT USAGE ON SCHEMA api TO authenticated_user;
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
GRANT anon TO authenticator; GRANT anon TO authenticator;
GRANT authenticated_user TO authenticator; GRANT authenticated_user TO authenticator;
CREATE SCHEMA internal; GRANT USAGE ON SCHEMA api TO anon;
GRANT USAGE ON SCHEMA api TO authenticated_user;
GRANT USAGE ON SCHEMA internal TO authenticated_user;
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
CREATE TABLE internal.user ( CREATE TABLE internal.user (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
@@ -30,8 +34,10 @@ CREATE TABLE internal.website (
content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL, content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL,
title VARCHAR(50) NOT NULL CHECK (TRIM(title) != ''), title VARCHAR(50) NOT NULL CHECK (TRIM(title) != ''),
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
is_published BOOLEAN NOT NULL DEFAULT FALSE,
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED
); );
CREATE TABLE internal.media ( CREATE TABLE internal.media (
@@ -70,19 +76,32 @@ CREATE TABLE internal.home (
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
); );
CREATE TABLE internal.docs_category (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
UNIQUE (website_id, category_name),
UNIQUE (website_id, category_weight)
);
CREATE TABLE internal.article ( CREATE TABLE internal.article (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL, website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL, user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
title VARCHAR(100) NOT NULL CHECK (TRIM(title) != ''), title VARCHAR(100) NOT NULL CHECK (TRIM(title) != ''),
meta_description VARCHAR(250) CHECK (TRIM(meta_description) != ''), meta_description VARCHAR(250) CHECK (TRIM(meta_description) != ''),
meta_author VARCHAR(100) CHECK (TRIM(meta_author) != ''), meta_author VARCHAR(100) CHECK (TRIM(meta_author) != ''),
cover_image UUID REFERENCES internal.media (id) ON DELETE SET NULL, cover_image UUID REFERENCES internal.media (id) ON DELETE SET NULL,
publication_date DATE NOT NULL DEFAULT CURRENT_DATE, publication_date DATE DEFAULT CURRENT_DATE,
main_content TEXT CHECK (TRIM(main_content) != ''), main_content TEXT CHECK (TRIM(main_content) != ''),
category UUID REFERENCES internal.docs_category (id) ON DELETE SET NULL,
article_weight INTEGER CHECK (article_weight IS NULL OR article_weight >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
title_description_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', COALESCE(title, '') || ' ' || COALESCE(meta_description, ''))) STORED
); );
CREATE TABLE internal.footer ( CREATE TABLE internal.footer (
@@ -92,6 +111,13 @@ CREATE TABLE internal.footer (
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
); );
CREATE TABLE internal.legal_information (
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
);
CREATE TABLE internal.collab ( CREATE TABLE internal.collab (
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE, website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE,
user_id UUID REFERENCES internal.user (id) ON DELETE CASCADE, user_id UUID REFERENCES internal.user (id) ON DELETE CASCADE,
@@ -117,10 +143,14 @@ DROP TABLE internal.change_log;
DROP TABLE internal.collab; DROP TABLE internal.collab;
DROP TABLE internal.legal_information;
DROP TABLE internal.footer; DROP TABLE internal.footer;
DROP TABLE internal.article; DROP TABLE internal.article;
DROP TABLE internal.docs_category;
DROP TABLE internal.home; DROP TABLE internal.home;
DROP TABLE internal.header; DROP TABLE internal.header;
@@ -131,15 +161,17 @@ DROP TABLE internal.media;
DROP TABLE internal.website; DROP TABLE internal.website;
DROP SCHEMA api;
DROP TABLE internal.user; DROP TABLE internal.user;
DROP SCHEMA internal; DROP SCHEMA api;
DROP ROLE authenticator; DROP SCHEMA internal;
DROP ROLE anon; DROP ROLE anon;
DROP ROLE authenticated_user; DROP ROLE authenticated_user;
DROP ROLE authenticator;
ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC;

View File

@@ -1,6 +1,6 @@
-- migrate:up -- migrate:up
CREATE FUNCTION pgrst_watch () CREATE FUNCTION pgrst_watch ()
RETURNS event_trigger RETURNS EVENT_TRIGGER
AS $$ AS $$
BEGIN BEGIN
NOTIFY pgrst, NOTIFY pgrst,

View File

@@ -45,23 +45,21 @@ CREATE TRIGGER encrypt_pass
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION internal.encrypt_pass (); EXECUTE FUNCTION internal.encrypt_pass ();
CREATE FUNCTION internal.user_role (username TEXT, PASSWORD TEXT) CREATE FUNCTION internal.user_role (username TEXT, pass TEXT, OUT role_name NAME)
RETURNS NAME AS $$
AS $$
BEGIN BEGIN
RETURN (
SELECT SELECT
ROLE ROLE INTO role_name
FROM FROM
internal.user AS u internal.user AS u
WHERE WHERE
u.username = user_role.username u.username = user_role.username
AND u.password_hash = CRYPT(user_role.password, u.password_hash)); AND u.password_hash = CRYPT(user_role.pass, u.password_hash);
END; END;
$$ $$
LANGUAGE plpgsql; LANGUAGE plpgsql;
CREATE FUNCTION api.register (username TEXT, PASSWORD TEXT, OUT user_id UUID) CREATE FUNCTION api.register (username TEXT, pass TEXT, OUT user_id UUID)
AS $$ AS $$
DECLARE DECLARE
_username_length_min CONSTANT INT := 3; _username_length_min CONSTANT INT := 3;
@@ -69,12 +67,11 @@ DECLARE
_password_length_min CONSTANT INT := 12; _password_length_min CONSTANT INT := 12;
_password_length_max CONSTANT INT := 128; _password_length_max CONSTANT INT := 128;
BEGIN BEGIN
IF LENGTH(register.username) CASE WHEN LENGTH(register.username)
NOT BETWEEN _username_length_min AND _username_length_max THEN NOT BETWEEN _username_length_min AND _username_length_max THEN
RAISE string_data_length_mismatch RAISE string_data_length_mismatch
USING message = FORMAT('Username must be between %s and %s characters long', _username_length_min, _username_length_max); USING message = FORMAT('Username must be between %s and %s characters long', _username_length_min, _username_length_max);
END IF; WHEN EXISTS (
IF EXISTS (
SELECT SELECT
1 1
FROM FROM
@@ -83,38 +80,35 @@ BEGIN
u.username = register.username) THEN u.username = register.username) THEN
RAISE unique_violation RAISE unique_violation
USING message = 'Username is already taken'; USING message = 'Username is already taken';
END IF; WHEN LENGTH(register.pass) NOT BETWEEN _password_length_min AND _password_length_max THEN
IF LENGTH(register.password)
NOT BETWEEN _password_length_min AND _password_length_max THEN
RAISE string_data_length_mismatch RAISE string_data_length_mismatch
USING message = FORMAT('Password must be between %s and %s characters long', _password_length_min, _password_length_max); USING message = FORMAT('Password must be between %s and %s characters long', _password_length_min, _password_length_max);
END IF; WHEN register.pass !~ '[a-z]' THEN
IF register.password !~ '[a-z]' THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'Password must contain at least one lowercase letter'; USING message = 'Password must contain at least one lowercase letter';
END IF; WHEN register.pass !~ '[A-Z]' THEN
IF register.password !~ '[A-Z]' THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'Password must contain at least one uppercase letter'; USING message = 'Password must contain at least one uppercase letter';
END IF; WHEN register.pass !~ '[0-9]' THEN
IF register.password !~ '[0-9]' THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'Password must contain at least one number'; USING message = 'Password must contain at least one number';
END IF; WHEN register.pass !~ '[!@#$%^&*(),.?":{}|<>]' THEN
IF register.password !~ '[!@#$%^&*(),.?":{}|<>]' THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'Password must contain at least one special character'; USING message = 'Password must contain at least one special character';
END IF; ELSE
INSERT INTO internal.user (username, password_hash) INSERT
VALUES (register.username, register.password) INTO internal.user (username, password_hash)
VALUES (register.username, register.pass)
RETURNING RETURNING
id INTO user_id; id INTO user_id;
END; END
CASE;
END;
$$ $$
LANGUAGE plpgsql LANGUAGE plpgsql
SECURITY DEFINER; SECURITY DEFINER;
CREATE FUNCTION api.login (username TEXT, PASSWORD TEXT, OUT token TEXT) CREATE FUNCTION api.login (username TEXT, pass TEXT, OUT token TEXT)
AS $$ AS $$
DECLARE DECLARE
_role NAME; _role NAME;
@@ -122,7 +116,7 @@ DECLARE
_exp INTEGER; _exp INTEGER;
BEGIN BEGIN
SELECT SELECT
internal.user_role (login.username, login.password) INTO _role; internal.user_role (login.username, login.pass) INTO _role;
IF _role IS NULL THEN IF _role IS NULL THEN
RAISE invalid_password RAISE invalid_password
USING message = 'Invalid username or password'; USING message = 'Invalid username or password';
@@ -141,14 +135,14 @@ $$
LANGUAGE plpgsql LANGUAGE plpgsql
SECURITY DEFINER; SECURITY DEFINER;
CREATE FUNCTION api.delete_account (PASSWORD TEXT, OUT was_deleted BOOLEAN) CREATE FUNCTION api.delete_account (pass TEXT, OUT was_deleted BOOLEAN)
AS $$ AS $$
DECLARE DECLARE
_username TEXT := CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'username'; _username TEXT := CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'username';
_role NAME; _role NAME;
BEGIN BEGIN
SELECT SELECT
internal.user_role (_username, delete_account.password) INTO _role; internal.user_role (_username, delete_account.pass) INTO _role;
IF _role IS NULL THEN IF _role IS NULL THEN
RAISE invalid_password RAISE invalid_password
USING message = 'Invalid password'; USING message = 'Invalid password';
@@ -165,7 +159,13 @@ GRANT EXECUTE ON FUNCTION api.register (TEXT, TEXT) TO anon;
GRANT EXECUTE ON FUNCTION api.login (TEXT, TEXT) TO anon; GRANT EXECUTE ON FUNCTION api.login (TEXT, TEXT) TO anon;
GRANT EXECUTE ON FUNCTION api.delete_account (TEXT) TO authenticated_user;
-- migrate:down -- migrate:down
DROP TRIGGER encrypt_pass ON internal.user;
DROP TRIGGER ensure_user_role_exists ON internal.user;
DROP FUNCTION api.register (TEXT, TEXT); DROP FUNCTION api.register (TEXT, TEXT);
DROP FUNCTION api.login (TEXT, TEXT); DROP FUNCTION api.login (TEXT, TEXT);
@@ -174,12 +174,8 @@ DROP FUNCTION api.delete_account (TEXT);
DROP FUNCTION internal.user_role (TEXT, TEXT); DROP FUNCTION internal.user_role (TEXT, TEXT);
DROP TRIGGER encrypt_pass ON internal.user;
DROP FUNCTION internal.encrypt_pass (); DROP FUNCTION internal.encrypt_pass ();
DROP TRIGGER ensure_user_role_exists ON internal.user;
DROP FUNCTION internal.check_role_exists (); DROP FUNCTION internal.check_role_exists ();
DROP EXTENSION pgjwt; DROP EXTENSION pgjwt;

View File

@@ -2,8 +2,7 @@
CREATE VIEW api.account WITH ( security_invoker = ON CREATE VIEW api.account WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
id, *
username
FROM FROM
internal.user internal.user
WHERE WHERE
@@ -23,99 +22,70 @@ FROM
CREATE VIEW api.website WITH ( security_invoker = ON CREATE VIEW api.website WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
id, *
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by
FROM FROM
internal.website; internal.website;
CREATE VIEW api.settings WITH ( security_invoker = ON CREATE VIEW api.settings WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
accent_color_light_theme,
accent_color_dark_theme,
favicon_image,
last_modified_at,
last_modified_by
FROM FROM
internal.settings; internal.settings;
CREATE VIEW api.header WITH ( security_invoker = ON CREATE VIEW api.header WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
logo_type,
logo_text,
logo_image,
last_modified_at,
last_modified_by
FROM FROM
internal.header; internal.header;
CREATE VIEW api.home WITH ( security_invoker = ON CREATE VIEW api.home WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
main_content,
last_modified_at,
last_modified_by
FROM FROM
internal.home; internal.home;
CREATE VIEW api.article WITH ( security_invoker = ON CREATE VIEW api.article WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
id, *
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by
FROM FROM
internal.article; internal.article;
CREATE VIEW api.docs_category WITH ( security_invoker = ON
) AS
SELECT
*
FROM
internal.docs_category;
CREATE VIEW api.footer WITH ( security_invoker = ON CREATE VIEW api.footer WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
additional_text,
last_modified_at,
last_modified_by
FROM FROM
internal.footer; internal.footer;
CREATE VIEW api.legal_information WITH ( security_invoker = ON
) AS
SELECT
*
FROM
internal.legal_information;
CREATE VIEW api.collab WITH ( security_invoker = ON CREATE VIEW api.collab WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
user_id,
permission_level,
added_at,
last_modified_at,
last_modified_by
FROM FROM
internal.collab; internal.collab;
CREATE VIEW api.change_log WITH ( security_invoker = ON CREATE VIEW api.change_log WITH ( security_invoker = ON
) AS ) AS
SELECT SELECT
website_id, *
user_id,
change_summary,
previous_value,
new_value,
timestamp
FROM FROM
internal.change_log; internal.change_log;
@@ -123,9 +93,8 @@ CREATE FUNCTION api.create_website (content_type VARCHAR(10), title VARCHAR(50),
AS $$ AS $$
DECLARE DECLARE
_website_id UUID; _website_id UUID;
_user_id UUID; _user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
BEGIN BEGIN
_user_id := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
INSERT INTO internal.website (content_type, title) INSERT INTO internal.website (content_type, title)
VALUES (create_website.content_type, create_website.title) VALUES (create_website.content_type, create_website.title)
RETURNING RETURNING
@@ -135,8 +104,7 @@ BEGIN
INSERT INTO internal.header (website_id, logo_text) INSERT INTO internal.header (website_id, logo_text)
VALUES (_website_id, 'archtika ' || create_website.content_type); VALUES (_website_id, 'archtika ' || create_website.content_type);
INSERT INTO internal.home (website_id, main_content) INSERT INTO internal.home (website_id, main_content)
VALUES (_website_id, ' VALUES (_website_id, '## About
## About
archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates. archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates.
@@ -148,8 +116,7 @@ For the backend, PostgreSQL is used in combination with PostgREST to create a RE
The web application uses SvelteKit with SSR (Server Side Rendering) and Svelte version 5, currently in beta. The web application uses SvelteKit with SSR (Server Side Rendering) and Svelte version 5, currently in beta.
NGINX is used to deploy the websites, serving the static site files from the `/var/www/archtika-websites` directory. The static files can be found in this directory via the path `<user_id>/<website_id>`, which is dynamically created by the web application. NGINX is used to deploy the websites, serving the static site files from the `/var/www/archtika-websites` directory. The static files can be found in this directory via the path `<user_id>/<website_id>`, which is dynamically created by the web application.');
');
INSERT INTO internal.footer (website_id, additional_text) INSERT INTO internal.footer (website_id, additional_text)
VALUES (_website_id, 'archtika is a free, open, modern, performant and lightweight CMS'); VALUES (_website_id, 'archtika is a free, open, modern, performant and lightweight CMS');
website_id := _website_id; website_id := _website_id;
@@ -187,10 +154,18 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON internal.article TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user; GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
GRANT SELECT, UPDATE ON internal.footer TO authenticated_user; GRANT SELECT, UPDATE ON internal.footer TO authenticated_user;
GRANT SELECT, UPDATE ON api.footer TO authenticated_user; GRANT SELECT, UPDATE ON api.footer TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.legal_information TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.collab TO authenticated_user; GRANT SELECT, INSERT, UPDATE, DELETE ON internal.collab TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user; GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user;
@@ -206,10 +181,14 @@ DROP VIEW api.change_log;
DROP VIEW api.collab; DROP VIEW api.collab;
DROP VIEW api.legal_information;
DROP VIEW api.footer; DROP VIEW api.footer;
DROP VIEW api.home; DROP VIEW api.home;
DROP VIEW api.docs_category;
DROP VIEW api.article; DROP VIEW api.article;
DROP VIEW api.header; DROP VIEW api.header;

View File

@@ -13,29 +13,30 @@ ALTER TABLE internal.home ENABLE ROW LEVEL SECURITY;
ALTER TABLE internal.article ENABLE ROW LEVEL SECURITY; ALTER TABLE internal.article ENABLE ROW LEVEL SECURITY;
ALTER TABLE internal.docs_category ENABLE ROW LEVEL SECURITY;
ALTER TABLE internal.footer ENABLE ROW LEVEL SECURITY; ALTER TABLE internal.footer ENABLE ROW LEVEL SECURITY;
ALTER TABLE internal.legal_information ENABLE ROW LEVEL SECURITY;
ALTER TABLE internal.collab ENABLE ROW LEVEL SECURITY; ALTER TABLE internal.collab ENABLE ROW LEVEL SECURITY;
CREATE FUNCTION internal.user_has_website_access (website_id UUID, required_permission INTEGER, collaborator_permission_level INTEGER DEFAULT NULL, collaborator_user_id UUID DEFAULT NULL, article_user_id UUID DEFAULT NULL, raise_error BOOLEAN DEFAULT TRUE) CREATE FUNCTION internal.user_has_website_access (website_id UUID, required_permission INTEGER, collaborator_permission_level INTEGER DEFAULT NULL, collaborator_user_id UUID DEFAULT NULL, article_user_id UUID DEFAULT NULL, raise_error BOOLEAN DEFAULT TRUE, OUT has_access BOOLEAN)
RETURNS BOOLEAN AS $$
AS $$
DECLARE DECLARE
_user_id UUID; _user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
_has_access BOOLEAN;
BEGIN BEGIN
_user_id := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
SELECT SELECT
EXISTS ( EXISTS (
SELECT SELECT
1 1
FROM FROM
internal.website internal.website AS w
WHERE WHERE
id = website_id w.id = user_has_website_access.website_id
AND user_id = _user_id) INTO _has_access; AND w.user_id = _user_id) INTO has_access;
IF _has_access THEN IF has_access THEN
RETURN _has_access; RETURN;
END IF; END IF;
SELECT SELECT
EXISTS ( EXISTS (
@@ -45,24 +46,25 @@ BEGIN
internal.collab c internal.collab c
WHERE WHERE
c.website_id = user_has_website_access.website_id c.website_id = user_has_website_access.website_id
AND c.user_id = (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID AND c.user_id = _user_id
AND c.permission_level >= user_has_website_access.required_permission AND c.permission_level >= user_has_website_access.required_permission
AND (user_has_website_access.article_user_id IS NULL AND (user_has_website_access.article_user_id IS NULL
OR (c.permission_level = 30 OR (c.permission_level = 30
OR user_has_website_access.article_user_id = _user_id)) OR user_has_website_access.article_user_id = _user_id))
AND (user_has_website_access.collaborator_permission_level IS NULL AND (user_has_website_access.collaborator_permission_level IS NULL
OR (user_has_website_access.collaborator_user_id != _user_id OR (user_has_website_access.collaborator_user_id != _user_id
AND user_has_website_access.collaborator_permission_level < 30))) INTO _has_access; AND user_has_website_access.collaborator_permission_level < 30))) INTO has_access;
IF NOT _has_access AND user_has_website_access.raise_error THEN IF NOT has_access AND user_has_website_access.raise_error THEN
RAISE insufficient_privilege RAISE insufficient_privilege
USING message = 'You do not have the required permissions for this action.'; USING message = 'You do not have the required permissions for this action.';
END IF; END IF;
RETURN _has_access;
END; END;
$$ $$
LANGUAGE plpgsql LANGUAGE plpgsql
SECURITY DEFINER; SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION internal.user_has_website_access (UUID, INTEGER, INTEGER, UUID, UUID, BOOLEAN) TO authenticated_user;
CREATE POLICY view_user ON internal.user CREATE POLICY view_user ON internal.user
FOR SELECT FOR SELECT
USING (TRUE); USING (TRUE);
@@ -127,6 +129,22 @@ CREATE POLICY insert_article ON internal.article
FOR INSERT FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 20)); WITH CHECK (internal.user_has_website_access (website_id, 20));
CREATE POLICY view_categories ON internal.docs_category
FOR SELECT
USING (internal.user_has_website_access (website_id, 10));
CREATE POLICY update_category ON internal.docs_category
FOR UPDATE
USING (internal.user_has_website_access (website_id, 20));
CREATE POLICY delete_category ON internal.docs_category
FOR DELETE
USING (internal.user_has_website_access (website_id, 20, article_user_id => user_id));
CREATE POLICY insert_category ON internal.docs_category
FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 20));
CREATE POLICY view_footer ON internal.footer CREATE POLICY view_footer ON internal.footer
FOR SELECT FOR SELECT
USING (internal.user_has_website_access (website_id, 10)); USING (internal.user_has_website_access (website_id, 10));
@@ -135,6 +153,22 @@ CREATE POLICY update_footer ON internal.footer
FOR UPDATE FOR UPDATE
USING (internal.user_has_website_access (website_id, 20)); USING (internal.user_has_website_access (website_id, 20));
CREATE POLICY view_legal_information ON internal.legal_information
FOR SELECT
USING (internal.user_has_website_access (website_id, 10));
CREATE POLICY update_legal_information ON internal.legal_information
FOR UPDATE
USING (internal.user_has_website_access (website_id, 30));
CREATE POLICY delete_legal_information ON internal.legal_information
FOR DELETE
USING (internal.user_has_website_access (website_id, 30));
CREATE POLICY insert_legal_information ON internal.legal_information
FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 30));
CREATE POLICY view_collaborations ON internal.collab CREATE POLICY view_collaborations ON internal.collab
FOR SELECT FOR SELECT
USING (internal.user_has_website_access (website_id, 10)); USING (internal.user_has_website_access (website_id, 10));
@@ -184,10 +218,26 @@ DROP POLICY delete_article ON internal.article;
DROP POLICY insert_article ON internal.article; DROP POLICY insert_article ON internal.article;
DROP POLICY view_categories ON internal.docs_category;
DROP POLICY update_category ON internal.docs_category;
DROP POLICY delete_category ON internal.docs_category;
DROP POLICY insert_category ON internal.docs_category;
DROP POLICY view_footer ON internal.footer; DROP POLICY view_footer ON internal.footer;
DROP POLICY update_footer ON internal.footer; DROP POLICY update_footer ON internal.footer;
DROP POLICY insert_legal_information ON internal.legal_information;
DROP POLICY delete_legal_information ON internal.legal_information;
DROP POLICY update_legal_information ON internal.legal_information;
DROP POLICY view_legal_information ON internal.legal_information;
DROP POLICY view_collaborations ON internal.collab; DROP POLICY view_collaborations ON internal.collab;
DROP POLICY insert_collaborations ON internal.collab; DROP POLICY insert_collaborations ON internal.collab;
@@ -212,7 +262,11 @@ ALTER TABLE internal.home DISABLE ROW LEVEL SECURITY;
ALTER TABLE internal.article DISABLE ROW LEVEL SECURITY; ALTER TABLE internal.article DISABLE ROW LEVEL SECURITY;
ALTER TABLE internal.docs_category DISABLE ROW LEVEL SECURITY;
ALTER TABLE internal.footer DISABLE ROW LEVEL SECURITY; ALTER TABLE internal.footer DISABLE ROW LEVEL SECURITY;
ALTER TABLE internal.legal_information DISABLE ROW LEVEL SECURITY;
ALTER TABLE internal.collab DISABLE ROW LEVEL SECURITY; ALTER TABLE internal.collab DISABLE ROW LEVEL SECURITY;

View File

@@ -1,40 +0,0 @@
-- migrate:up
CREATE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;
-- migrate:down
DROP VIEW api.website_overview;

View File

@@ -12,25 +12,12 @@ BEGIN
last_modified_at = NEW.last_modified_at, last_modified_at = NEW.last_modified_at,
last_modified_by = NEW.last_modified_by last_modified_by = NEW.last_modified_by
WHERE WHERE
id = CASE WHEN TG_TABLE_NAME = 'settings' THEN id = NEW.website_id;
NEW.website_id
WHEN TG_TABLE_NAME = 'header' THEN
NEW.website_id
WHEN TG_TABLE_NAME = 'home' THEN
NEW.website_id
WHEN TG_TABLE_NAME = 'article' THEN
NEW.website_id
WHEN TG_TABLE_NAME = 'footer' THEN
NEW.website_id
WHEN TG_TABLE_NAME = 'collab' THEN
NEW.website_id
END;
END IF; END IF;
RETURN NEW; RETURN NEW;
END; END;
$$ $$
LANGUAGE plpgsql LANGUAGE plpgsql;
SECURITY DEFINER;
CREATE TRIGGER update_website_last_modified CREATE TRIGGER update_website_last_modified
BEFORE UPDATE ON internal.website BEFORE UPDATE ON internal.website
@@ -62,6 +49,11 @@ CREATE TRIGGER update_footer_last_modified
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION internal.update_last_modified (); EXECUTE FUNCTION internal.update_last_modified ();
CREATE TRIGGER update_legal_information_last_modified
BEFORE INSERT OR UPDATE OR DELETE ON internal.legal_information
FOR EACH ROW
EXECUTE FUNCTION internal.update_last_modified ();
CREATE TRIGGER update_collab_last_modified CREATE TRIGGER update_collab_last_modified
BEFORE UPDATE ON internal.collab BEFORE UPDATE ON internal.collab
FOR EACH ROW FOR EACH ROW
@@ -80,6 +72,8 @@ DROP TRIGGER update_article_last_modified ON internal.article;
DROP TRIGGER update_footer_last_modified ON internal.footer; DROP TRIGGER update_footer_last_modified ON internal.footer;
DROP TRIGGER update_legal_information_last_modified ON internal.legal_information;
DROP TRIGGER update_collab_last_modified ON internal.collab; DROP TRIGGER update_collab_last_modified ON internal.collab;
DROP FUNCTION internal.update_last_modified (); DROP FUNCTION internal.update_last_modified ();

View File

@@ -7,10 +7,10 @@ BEGIN
SELECT SELECT
1 1
FROM FROM
internal.website internal.website AS w
WHERE WHERE
id = NEW.website_id w.id = NEW.website_id
AND user_id = NEW.user_id) THEN AND w.user_id = NEW.user_id) THEN
RAISE foreign_key_violation RAISE foreign_key_violation
USING message = 'User cannot be added as a collaborator to their own website'; USING message = 'User cannot be added as a collaborator to their own website';
END IF; END IF;

View File

@@ -10,7 +10,9 @@ DECLARE
_original_filename TEXT := _headers ->> 'x-original-filename'; _original_filename TEXT := _headers ->> 'x-original-filename';
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp']; _allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
_max_file_size INT := 5 * 1024 * 1024; _max_file_size INT := 5 * 1024 * 1024;
_has_access BOOLEAN;
BEGIN BEGIN
_has_access = internal.user_has_website_access (_website_id, 20);
IF OCTET_LENGTH($1) = 0 THEN IF OCTET_LENGTH($1) = 0 THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'No file data was provided'; USING message = 'No file data was provided';
@@ -19,7 +21,7 @@ BEGIN
SELECT SELECT
UNNEST(_allowed_mimetypes)) THEN UNNEST(_allowed_mimetypes)) THEN
RAISE invalid_parameter_value RAISE invalid_parameter_value
USING message = 'Invalid MIME type. Allowed types are: png, svg, jpg, webp'; USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
END IF; END IF;
IF OCTET_LENGTH($1) > _max_file_size THEN IF OCTET_LENGTH($1) > _max_file_size THEN
RAISE program_limit_exceeded RAISE program_limit_exceeded
@@ -46,7 +48,7 @@ BEGIN
'{ "Content-Disposition": "inline; filename=\"%s\"" },' '{ "Content-Disposition": "inline; filename=\"%s\"" },'
'{ "Cache-Control": "max-age=259200" }]', m.mimetype, m.original_name) '{ "Cache-Control": "max-age=259200" }]', m.mimetype, m.original_name)
FROM FROM
internal.media m internal.media AS m
WHERE WHERE
m.id = retrieve_file.id INTO _headers; m.id = retrieve_file.id INTO _headers;
PERFORM PERFORM

View File

@@ -1,90 +0,0 @@
-- migrate:up
ALTER TABLE internal.website
ADD COLUMN title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED;
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by,
title_search -- New column
FROM
internal.website;
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
ALTER TABLE internal.article
ADD COLUMN title_description_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', COALESCE(title, '') || ' ' || COALESCE(meta_description, ''))) STORED;
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
title_description_search -- New column
FROM
internal.article;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
-- migrate:down
DROP VIEW api.article;
CREATE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by
FROM
internal.article;
ALTER TABLE internal.article
DROP COLUMN title_description_search;
DROP VIEW api.website;
CREATE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by
FROM
internal.website;
ALTER TABLE internal.website
DROP COLUMN title_search;
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;

View File

@@ -1,74 +0,0 @@
-- migrate:up
CREATE OR REPLACE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
AS $$
DECLARE
_headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
_mimetype TEXT := _headers ->> 'x-mimetype';
_original_filename TEXT := _headers ->> 'x-original-filename';
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
_max_file_size INT := 5 * 1024 * 1024;
BEGIN
IF OCTET_LENGTH($1) = 0 THEN
RAISE invalid_parameter_value
USING message = 'No file data was provided';
END IF;
IF _mimetype IS NULL OR _mimetype NOT IN (
SELECT
UNNEST(_allowed_mimetypes)) THEN
RAISE invalid_parameter_value
USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
END IF;
IF OCTET_LENGTH($1) > _max_file_size THEN
RAISE program_limit_exceeded
USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
END IF;
INSERT INTO internal.media (website_id, blob, mimetype, original_name)
VALUES (_website_id, $1, _mimetype, _original_filename)
RETURNING
id INTO file_id;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;
-- migrate:down
DROP FUNCTION api.upload_file (BYTEA);
CREATE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
AS $$
DECLARE
_headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
_mimetype TEXT := _headers ->> 'x-mimetype';
_original_filename TEXT := _headers ->> 'x-original-filename';
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
_max_file_size INT := 5 * 1024 * 1024;
BEGIN
IF OCTET_LENGTH($1) = 0 THEN
RAISE invalid_parameter_value
USING message = 'No file data was provided';
END IF;
IF _mimetype IS NULL OR _mimetype NOT IN (
SELECT
UNNEST(_allowed_mimetypes)) THEN
RAISE invalid_parameter_value
USING message = 'Invalid MIME type. Allowed types are: png, svg, jpg, webp';
END IF;
IF OCTET_LENGTH($1) > _max_file_size THEN
RAISE program_limit_exceeded
USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
END IF;
INSERT INTO internal.media (website_id, blob, mimetype, original_name)
VALUES (_website_id, $1, _mimetype, _original_filename)
RETURNING
id INTO file_id;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;

View File

@@ -1,120 +0,0 @@
-- migrate:up
CREATE TABLE internal.docs_category (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
UNIQUE (website_id, category_name),
UNIQUE (website_id, category_weight)
);
ALTER TABLE internal.website
ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE internal.article
ADD COLUMN category UUID REFERENCES internal.docs_category (id) ON DELETE SET NULL;
ALTER TABLE internal.article
ALTER COLUMN user_id SET DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
ALTER TABLE internal.docs_category ENABLE ROW LEVEL SECURITY;
CREATE POLICY view_categories ON internal.docs_category
FOR SELECT
USING (internal.user_has_website_access (website_id, 10));
CREATE POLICY update_category ON internal.docs_category
FOR UPDATE
USING (internal.user_has_website_access (website_id, 20));
CREATE POLICY delete_category ON internal.docs_category
FOR DELETE
USING (internal.user_has_website_access (website_id, 20, article_user_id => user_id));
CREATE POLICY insert_category ON internal.docs_category
FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 20));
CREATE VIEW api.docs_category WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
category_name,
category_weight
FROM
internal.docs_category;
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
title_description_search,
category -- New column
FROM
internal.article;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
-- migrate:down
DROP POLICY view_categories ON internal.docs_category;
DROP POLICY update_category ON internal.docs_category;
DROP POLICY delete_category ON internal.docs_category;
DROP POLICY insert_category ON internal.docs_category;
DROP VIEW api.article;
CREATE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
title_description_search
FROM
internal.article;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
DROP VIEW api.docs_category;
ALTER TABLE internal.article
DROP COLUMN category;
DROP TABLE internal.docs_category;
ALTER TABLE internal.website
DROP COLUMN is_published;
ALTER TABLE internal.article
ALTER COLUMN user_id DROP DEFAULT;

View File

@@ -1,108 +0,0 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;
-- migrate:down
DROP VIEW api.website_overview;
CREATE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;

View File

@@ -1,56 +0,0 @@
-- migrate:up
ALTER TABLE internal.article
ADD COLUMN article_weight INTEGER CHECK (article_weight IS NULL
OR article_weight >= 0);
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
title_description_search,
category,
article_weight -- New column
FROM
internal.article;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
-- migrate:down
DROP VIEW api.article;
CREATE VIEW api.article WITH ( security_invoker = ON
) AS
SELECT
id,
website_id,
user_id,
title,
meta_description,
meta_author,
cover_image,
publication_date,
main_content,
created_at,
last_modified_at,
last_modified_by,
title_description_search,
category
FROM
internal.article;
ALTER TABLE internal.article
DROP COLUMN article_weight;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;

View File

@@ -1,140 +0,0 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC NULLS LAST
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;
-- migrate:down
DROP VIEW api.website_overview;
CREATE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;

View File

@@ -1,140 +0,0 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
) ORDER BY a.article_weight DESC NULLS LAST
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC NULLS LAST
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;
-- migrate:down
DROP VIEW api.website_overview;
CREATE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC NULLS LAST
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;

View File

@@ -1,15 +0,0 @@
-- migrate:up
ALTER TABLE internal.article
ALTER COLUMN publication_date DROP NOT NULL;
-- migrate:down
UPDATE
internal.article
SET
publication_date = CURRENT_DATE
WHERE
publication_date IS NULL;
ALTER TABLE internal.article
ALTER COLUMN publication_date SET NOT NULL;

View File

@@ -1,37 +0,0 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by,
title_search,
is_published -- New column
FROM
internal.website;
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
-- migrate:down
DROP VIEW api.website;
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
) AS
SELECT
id,
user_id,
content_type,
title,
created_at,
last_modified_at,
last_modified_by,
title_search
FROM
internal.website;
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;

View File

@@ -1,76 +0,0 @@
-- migrate:up
CREATE OR REPLACE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
AS $$
DECLARE
_headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
_mimetype TEXT := _headers ->> 'x-mimetype';
_original_filename TEXT := _headers ->> 'x-original-filename';
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
_max_file_size INT := 5 * 1024 * 1024;
_has_access BOOLEAN;
BEGIN
_has_access = internal.user_has_website_access (_website_id, 20);
IF OCTET_LENGTH($1) = 0 THEN
RAISE invalid_parameter_value
USING message = 'No file data was provided';
END IF;
IF _mimetype IS NULL OR _mimetype NOT IN (
SELECT
UNNEST(_allowed_mimetypes)) THEN
RAISE invalid_parameter_value
USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
END IF;
IF OCTET_LENGTH($1) > _max_file_size THEN
RAISE program_limit_exceeded
USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
END IF;
INSERT INTO internal.media (website_id, blob, mimetype, original_name)
VALUES (_website_id, $1, _mimetype, _original_filename)
RETURNING
id INTO file_id;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;
-- migrate:down
DROP FUNCTION api.upload_file (BYTEA);
CREATE FUNCTION api.upload_file (BYTEA, OUT file_id UUID)
AS $$
DECLARE
_headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON;
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
_mimetype TEXT := _headers ->> 'x-mimetype';
_original_filename TEXT := _headers ->> 'x-original-filename';
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
_max_file_size INT := 5 * 1024 * 1024;
BEGIN
IF OCTET_LENGTH($1) = 0 THEN
RAISE invalid_parameter_value
USING message = 'No file data was provided';
END IF;
IF _mimetype IS NULL OR _mimetype NOT IN (
SELECT
UNNEST(_allowed_mimetypes)) THEN
RAISE invalid_parameter_value
USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp';
END IF;
IF OCTET_LENGTH($1) > _max_file_size THEN
RAISE program_limit_exceeded
USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024));
END IF;
INSERT INTO internal.media (website_id, blob, mimetype, original_name)
VALUES (_website_id, $1, _mimetype, _original_filename)
RETURNING
id INTO file_id;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user;

View File

@@ -1,55 +0,0 @@
-- migrate:up
CREATE TABLE internal.legal_information (
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
);
CREATE VIEW api.legal_information WITH ( security_invoker = ON
) AS
SELECT
website_id,
main_content,
last_modified_at,
last_modified_by
FROM
internal.legal_information;
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.legal_information TO authenticated_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
ALTER TABLE internal.legal_information ENABLE ROW LEVEL SECURITY;
CREATE POLICY view_legal_information ON internal.legal_information
FOR SELECT
USING (internal.user_has_website_access (website_id, 10));
CREATE POLICY update_legal_information ON internal.legal_information
FOR UPDATE
USING (internal.user_has_website_access (website_id, 30));
CREATE POLICY delete_legal_information ON internal.legal_information
FOR DELETE
USING (internal.user_has_website_access (website_id, 30));
CREATE POLICY insert_legal_information ON internal.legal_information
FOR INSERT
WITH CHECK (internal.user_has_website_access (website_id, 30));
-- migrate:down
DROP POLICY insert_legal_information ON internal.legal_information;
DROP POLICY delete_legal_information ON internal.legal_information;
DROP POLICY update_legal_information ON internal.legal_information;
DROP POLICY view_legal_information ON internal.legal_information;
ALTER TABLE internal.legal_information DISABLE ROW LEVEL SECURITY;
DROP VIEW api.legal_information;
DROP TABLE internal.legal_information;

View File

@@ -1,142 +0,0 @@
-- migrate:up
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
) ORDER BY a.article_weight DESC NULLS LAST
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC NULLS LAST
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles,
li.main_content legal_information_main_content
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id
LEFT JOIN internal.legal_information li ON w.id = li.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;
-- migrate:down
DROP VIEW api.website_overview;
CREATE VIEW api.website_overview WITH ( security_invoker = ON
) AS
SELECT
w.id,
w.user_id,
w.content_type,
w.title,
s.accent_color_light_theme,
s.accent_color_dark_theme,
s.favicon_image,
h.logo_type,
h.logo_text,
h.logo_image,
ho.main_content,
f.additional_text,
(
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
)
)
FROM
internal.article a
WHERE
a.website_id = w.id
) AS articles,
CASE WHEN w.content_type = 'Docs' THEN
(
SELECT
JSON_OBJECT_AGG(
COALESCE(
category_name, 'Uncategorized'
), articles
)
FROM (
SELECT
dc.category_name,
dc.category_weight AS category_weight,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
) ORDER BY a.article_weight DESC NULLS LAST
) AS articles
FROM
internal.article a
LEFT JOIN internal.docs_category dc ON a.category = dc.id
WHERE
a.website_id = w.id
GROUP BY
dc.id,
dc.category_name,
dc.category_weight
ORDER BY
category_weight DESC NULLS LAST
) AS categorized_articles)
ELSE
NULL
END AS categorized_articles
FROM
internal.website w
JOIN internal.settings s ON w.id = s.website_id
JOIN internal.header h ON w.id = h.website_id
JOIN internal.home ho ON w.id = ho.website_id
JOIN internal.footer f ON w.id = f.website_id;
GRANT SELECT ON api.website_overview TO authenticated_user;

View File

@@ -28,6 +28,7 @@
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-svelte": "2.43.0", "eslint-plugin-svelte": "2.43.0",
"globals": "15.9.0", "globals": "15.9.0",
"pg-to-ts": "4.1.1",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-svelte": "3.2.6", "prettier-plugin-svelte": "3.2.6",
"svelte": "5.0.0-next.220", "svelte": "5.0.0-next.220",
@@ -1631,6 +1632,16 @@
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
}, },
"node_modules/assert-options": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.0.tgz",
"integrity": "sha512-qSELrEaEz4sGwTs4Qh+swQkjiHAysC4rot21+jzXU86dJzNG+FDqBzyS3ohSoTRf4ZLA3FSwxQdiuNl5NXUtvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1700,6 +1711,16 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/builtin-modules": { "node_modules/builtin-modules": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
@@ -1781,6 +1802,100 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1813,6 +1928,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/commandpost": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz",
"integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -1985,6 +2114,43 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/editorconfig": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
"integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^2.19.0",
"lru-cache": "^4.1.5",
"semver": "^5.6.0",
"sigmund": "^1.0.1"
},
"bin": {
"editorconfig": "bin/editorconfig"
}
},
"node_modules/editorconfig/node_modules/lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"license": "ISC",
"dependencies": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"node_modules/editorconfig/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -2050,6 +2216,16 @@
"@esbuild/win32-x64": "0.21.5" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2541,6 +2717,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/github-slugger": { "node_modules/github-slugger": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
@@ -3077,6 +3263,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3361,6 +3554,13 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"dev": true,
"license": "MIT"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3440,6 +3640,142 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
"integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-minify": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.6.2.tgz",
"integrity": "sha512-1KdmFGGTP6jplJoI8MfvRlfvMiyBivMRP7/ffh4a11RUFJ7kC2J0ZHlipoKiH/1hz+DVgceon9U2qbaHpPeyPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
"integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-promise": {
"version": "10.15.4",
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.15.4.tgz",
"integrity": "sha512-BKlHCMCdNUmF6gagVbehRWSEiVcZzPVltEx14OJExR9Iz9/1R6KETDWLLGv2l6yRqYFnEZZy1VDjRhArzeIGrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"assert-options": "0.8.0",
"pg": "8.8.0",
"pg-minify": "1.6.2",
"spex": "3.2.0"
},
"engines": {
"node": ">=12.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
"integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-to-ts": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/pg-to-ts/-/pg-to-ts-4.1.1.tgz",
"integrity": "sha512-wc/ZXMMQrxu42mnl6eEdMgT31S9rvA/Oh9I9PchovUwoJLzEg0osGQjxiQOLjAdz3Ti45o749XREJ2s+xncZ6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"pg-promise": "^10.11.1",
"typescript-formatter": "^7.0.1",
"yargs": "^17.3.1"
},
"bin": {
"pg-to-ts": "dist/cli.js"
},
"engines": {
"node": ">=8.15.1"
},
"peerDependencies": {
"typescript": ">=4.1"
}
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -3624,6 +3960,49 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3661,6 +4040,13 @@
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
} }
}, },
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
"dev": true,
"license": "ISC"
},
"node_modules/psl": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -3716,6 +4102,16 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -3974,6 +4370,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -4028,6 +4431,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/spex": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spex/-/spex-3.2.0.tgz",
"integrity": "sha512-9srjJM7NaymrpwMHvSmpDeIK5GoRMX/Tq0E8aOlDPS54dDnDUIp30DrP9SphMPEETDLzEM9+4qo+KipmbtPecg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.5"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -4517,6 +4940,26 @@
} }
} }
}, },
"node_modules/typescript-formatter": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz",
"integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"commandpost": "^1.0.0",
"editorconfig": "^0.15.0"
},
"bin": {
"tsfmt": "bin/tsfmt"
},
"engines": {
"node": ">= 4.2.0"
},
"peerDependencies": {
"typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
@@ -4857,6 +5300,33 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true,
"license": "ISC"
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
@@ -4867,6 +5337,80 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -10,7 +10,8 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write ." "format": "prettier --write .",
"gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.40.0", "@playwright/test": "1.40.0",
@@ -26,6 +27,7 @@
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-svelte": "2.43.0", "eslint-plugin-svelte": "2.43.0",
"globals": "15.9.0", "globals": "15.9.0",
"pg-to-ts": "4.1.1",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-svelte": "3.2.6", "prettier-plugin-svelte": "3.2.6",
"svelte": "5.0.0-next.220", "svelte": "5.0.0-next.220",

View File

@@ -1,9 +1,6 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
interface User { import type { User } from "$lib/db-schema";
id: string;
username: string;
}
declare global { declare global {
namespace App { namespace App {

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
const { date }: { date: string } = $props(); const { date }: { date: Date } = $props();
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: "numeric", year: "numeric",
@@ -11,6 +11,6 @@
}; };
</script> </script>
<time datetime={new Date(date).toLocaleString("sv").replace(" ", "T")}> <time datetime={date.toLocaleString("sv").replace(" ", "T")}>
{new Date(date).toLocaleString("en-us", { ...options })} {date.toLocaleString("en-us", { ...options })}
</time> </time>

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
const { success, message }: { success: boolean | undefined; message: string | undefined } = const { success, message }: { success?: boolean; message?: string } = $props();
$props();
</script> </script>
{#if success} {#if success}

View File

@@ -0,0 +1,496 @@
/* tslint:disable */
/* eslint-disable */
/**
* AUTO-GENERATED FILE - DO NOT EDIT!
*
* This file was automatically generated by pg-to-ts v.4.1.1
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
*
*/
export type Json = unknown;
// Table article
export interface Article {
id: string;
website_id: string;
user_id: string | null;
title: string;
meta_description: string | null;
meta_author: string | null;
cover_image: string | null;
publication_date: Date | null;
main_content: string | null;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
title_description_search: any | null;
category: string | null;
article_weight: number | null;
}
export interface ArticleInput {
id?: string;
website_id: string;
user_id?: string | null;
title: string;
meta_description?: string | null;
meta_author?: string | null;
cover_image?: string | null;
publication_date?: Date | null;
main_content?: string | null;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
title_description_search?: any | null;
category?: string | null;
article_weight?: number | null;
}
const article = {
tableName: "article",
columns: [
"id",
"website_id",
"user_id",
"title",
"meta_description",
"meta_author",
"cover_image",
"publication_date",
"main_content",
"created_at",
"last_modified_at",
"last_modified_by",
"title_description_search",
"category",
"article_weight"
],
requiredForInsert: ["website_id", "title"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User },
cover_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User },
category: { table: "docs_category", column: "id", $type: null as unknown as DocsCategory }
},
$type: null as unknown as Article,
$input: null as unknown as ArticleInput
} as const;
// Table change_log
export interface ChangeLog {
website_id: string;
user_id: string;
change_summary: string;
previous_value: Json | null;
new_value: Json | null;
timestamp: Date;
}
export interface ChangeLogInput {
website_id: string;
user_id?: string;
change_summary: string;
previous_value?: Json | null;
new_value?: Json | null;
timestamp?: Date;
}
const change_log = {
tableName: "change_log",
columns: ["website_id", "user_id", "change_summary", "previous_value", "new_value", "timestamp"],
requiredForInsert: ["website_id", "change_summary"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as ChangeLog,
$input: null as unknown as ChangeLogInput
} as const;
// Table collab
export interface Collab {
website_id: string;
user_id: string;
permission_level: number;
added_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface CollabInput {
website_id: string;
user_id: string;
permission_level?: number;
added_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const collab = {
tableName: "collab",
columns: [
"website_id",
"user_id",
"permission_level",
"added_at",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id", "user_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Collab,
$input: null as unknown as CollabInput
} as const;
// Table docs_category
export interface DocsCategory {
id: string;
website_id: string;
user_id: string | null;
category_name: string;
category_weight: number;
}
export interface DocsCategoryInput {
id?: string;
website_id: string;
user_id?: string | null;
category_name: string;
category_weight: number;
}
const docs_category = {
tableName: "docs_category",
columns: ["id", "website_id", "user_id", "category_name", "category_weight"],
requiredForInsert: ["website_id", "category_name", "category_weight"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as DocsCategory,
$input: null as unknown as DocsCategoryInput
} as const;
// Table footer
export interface Footer {
website_id: string;
additional_text: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface FooterInput {
website_id: string;
additional_text: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const footer = {
tableName: "footer",
columns: ["website_id", "additional_text", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "additional_text"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Footer,
$input: null as unknown as FooterInput
} as const;
// Table header
export interface Header {
website_id: string;
logo_type: string;
logo_text: string | null;
logo_image: string | null;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface HeaderInput {
website_id: string;
logo_type?: string;
logo_text?: string | null;
logo_image?: string | null;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const header = {
tableName: "header",
columns: [
"website_id",
"logo_type",
"logo_text",
"logo_image",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
logo_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Header,
$input: null as unknown as HeaderInput
} as const;
// Table home
export interface Home {
website_id: string;
main_content: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface HomeInput {
website_id: string;
main_content: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const home = {
tableName: "home",
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "main_content"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Home,
$input: null as unknown as HomeInput
} as const;
// Table legal_information
export interface LegalInformation {
website_id: string;
main_content: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface LegalInformationInput {
website_id: string;
main_content: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const legal_information = {
tableName: "legal_information",
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "main_content"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as LegalInformation,
$input: null as unknown as LegalInformationInput
} as const;
// Table media
export interface Media {
id: string;
website_id: string;
user_id: string;
blob: string;
mimetype: string;
original_name: string;
created_at: Date;
}
export interface MediaInput {
id?: string;
website_id: string;
user_id?: string;
blob: string;
mimetype: string;
original_name: string;
created_at?: Date;
}
const media = {
tableName: "media",
columns: ["id", "website_id", "user_id", "blob", "mimetype", "original_name", "created_at"],
requiredForInsert: ["website_id", "blob", "mimetype", "original_name"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Media,
$input: null as unknown as MediaInput
} as const;
// Table settings
export interface Settings {
website_id: string;
accent_color_light_theme: string;
accent_color_dark_theme: string;
favicon_image: string | null;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface SettingsInput {
website_id: string;
accent_color_light_theme?: string;
accent_color_dark_theme?: string;
favicon_image?: string | null;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const settings = {
tableName: "settings",
columns: [
"website_id",
"accent_color_light_theme",
"accent_color_dark_theme",
"favicon_image",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
favicon_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Settings,
$input: null as unknown as SettingsInput
} as const;
// Table user
export interface User {
id: string;
username: string;
password_hash: string;
role: string;
}
export interface UserInput {
id?: string;
username: string;
password_hash: string;
role?: string;
}
const user = {
tableName: "user",
columns: ["id", "username", "password_hash", "role"],
requiredForInsert: ["username", "password_hash"],
primaryKey: "id",
foreignKeys: {},
$type: null as unknown as User,
$input: null as unknown as UserInput
} as const;
// Table website
export interface Website {
id: string;
user_id: string;
content_type: string;
title: string;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
title_search: any | null;
is_published: boolean;
}
export interface WebsiteInput {
id?: string;
user_id?: string;
content_type: string;
title: string;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
title_search?: any | null;
is_published?: boolean;
}
const website = {
tableName: "website",
columns: [
"id",
"user_id",
"content_type",
"title",
"created_at",
"last_modified_at",
"last_modified_by",
"title_search",
"is_published"
],
requiredForInsert: ["content_type", "title"],
primaryKey: "id",
foreignKeys: {
user_id: { table: "user", column: "id", $type: null as unknown as User },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Website,
$input: null as unknown as WebsiteInput
} as const;
export interface TableTypes {
article: {
select: Article;
input: ArticleInput;
};
change_log: {
select: ChangeLog;
input: ChangeLogInput;
};
collab: {
select: Collab;
input: CollabInput;
};
docs_category: {
select: DocsCategory;
input: DocsCategoryInput;
};
footer: {
select: Footer;
input: FooterInput;
};
header: {
select: Header;
input: HeaderInput;
};
home: {
select: Home;
input: HomeInput;
};
legal_information: {
select: LegalInformation;
input: LegalInformationInput;
};
media: {
select: Media;
input: MediaInput;
};
settings: {
select: Settings;
input: SettingsInput;
};
user: {
select: User;
input: UserInput;
};
website: {
select: Website;
input: WebsiteInput;
};
}
export const tables = {
article,
change_log,
collab,
docs_category,
footer,
header,
home,
legal_information,
media,
settings,
user,
website
};

View File

@@ -2,52 +2,44 @@
import Head from "../common/Head.svelte"; import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte"; import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte"; import Footer from "../common/Footer.svelte";
import { type WebsiteOverview, md } from "../../utils";
import type { Article } from "../../db-schema";
const { const {
favicon, websiteOverview,
title, article,
logoType, apiUrl
logo, }: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
mainContent,
coverImage,
publicationDate,
footerAdditionalText,
metaDescription
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
coverImage: string;
publicationDate: string;
footerAdditionalText: string;
metaDescription: string;
} = $props();
</script> </script>
<Head {title} {favicon} nestingLevel={1} {metaDescription} /> <Head
{websiteOverview}
nestingLevel={1}
{apiUrl}
title={article.title}
metaDescription={article.meta_description}
/>
<Nav {logoType} {logo} isIndexPage={false} /> <Nav {websiteOverview} isDocsTemplate={false} isIndexPage={false} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">
<hgroup> <hgroup>
<p>{publicationDate}</p> <p>{article.publication_date}</p>
<h1>{title}</h1> <h1>{article.title}</h1>
</hgroup> </hgroup>
{#if coverImage} {#if article.cover_image}
<img src={coverImage} alt="" /> <img src="{apiUrl}/rpc/retrieve_file?id={article.cover_image}" alt="" />
{/if} {/if}
</div> </div>
</header> </header>
{#if mainContent} {#if article.main_content}
<main> <main>
<div class="container"> <div class="container">
{@html mainContent} {@html md(article.main_content)}
</div> </div>
</main> </main>
{/if} {/if}
<Footer text={footerAdditionalText} isIndexPage={false} /> <Footer {websiteOverview} isIndexPage={false} />

View File

@@ -2,47 +2,46 @@
import Head from "../common/Head.svelte"; import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte"; import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte"; import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
const { const {
favicon, websiteOverview,
title, apiUrl,
logoType, isLegalPage
logo, }: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
mainContent,
articles,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
articles: { title: string; publication_date: string; meta_description: string }[];
footerAdditionalText: string;
} = $props();
</script> </script>
<Head {title} {favicon} /> <Head
{websiteOverview}
nestingLevel={0}
{apiUrl}
title={isLegalPage ? "Legal information" : websiteOverview.title}
/>
<Nav {logoType} {logo} /> <Nav {websiteOverview} isDocsTemplate={false} isIndexPage={true} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">
<h1>{title}</h1> <h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
</div> </div>
</header> </header>
<main> <main>
<div class="container"> <div class="container">
{@html mainContent} {@html md(
{#if articles.length > 0} isLegalPage
? (websiteOverview.legal_information?.main_content ?? "")
: websiteOverview.home.main_content,
false
)}
{#if websiteOverview.article.length > 0 && !isLegalPage}
<section class="articles" id="articles"> <section class="articles" id="articles">
<h2> <h2>
<a href="#articles">Articles</a> <a href="#articles">Articles</a>
</h2> </h2>
<ul class="unpadded"> <ul class="unpadded">
{#each articles as article} {#each websiteOverview.article as article}
{@const articleFileName = article.title.toLowerCase().split(" ").join("-")} {@const articleFileName = article.title.toLowerCase().split(" ").join("-")}
<li> <li>
<p>{article.publication_date}</p> <p>{article.publication_date}</p>
@@ -62,4 +61,4 @@
</div> </div>
</main> </main>
<Footer text={footerAdditionalText} /> <Footer {websiteOverview} isIndexPage={true} />

View File

@@ -1,11 +1,16 @@
<script lang="ts"> <script lang="ts">
const { text, isIndexPage = true }: { text: string; isIndexPage?: boolean } = $props(); import type { WebsiteOverview } from "../../utils";
const {
websiteOverview,
isIndexPage
}: { websiteOverview: WebsiteOverview; isIndexPage: boolean } = $props();
</script> </script>
<footer> <footer>
<div class="container"> <div class="container">
<small> <small>
{@html text.replace( {@html websiteOverview.footer.additional_text.replace(
"!!legal", "!!legal",
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>` `<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
)} )}

View File

@@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { WebsiteOverview } from "../../utils";
const { const {
websiteOverview,
nestingLevel,
apiUrl,
title, title,
favicon, metaDescription
nestingLevel = 0,
metaDescription = null
}: { }: {
websiteOverview: WebsiteOverview;
nestingLevel: number;
apiUrl: string;
title: string; title: string;
favicon: string;
nestingLevel?: number;
metaDescription?: string | null; metaDescription?: string | null;
} = $props(); } = $props();
</script> </script>
@@ -19,8 +23,11 @@
<title>{title}</title> <title>{title}</title>
<meta name="description" content={metaDescription ?? title} /> <meta name="description" content={metaDescription ?? title} />
<link rel="stylesheet" href={`${"../".repeat(nestingLevel)}styles.css`} /> <link rel="stylesheet" href={`${"../".repeat(nestingLevel)}styles.css`} />
{#if favicon} {#if websiteOverview.settings.favicon_image}
<link rel="icon" href={favicon} /> <link
rel="icon"
href="{apiUrl}/rpc/retrieve_file?id={websiteOverview.settings.favicon_image}"
/>
{/if} {/if}
</head> </head>
</svelte:head> </svelte:head>

View File

@@ -1,17 +1,36 @@
<script lang="ts"> <script lang="ts">
import type { WebsiteOverview } from "../../utils";
import type { Article } from "../../db-schema";
const { const {
logoType, websiteOverview,
logo, isDocsTemplate,
isDocsTemplate = false, isIndexPage,
categorizedArticles = {}, apiUrl
isIndexPage = true
}: { }: {
logoType: "text" | "image"; websiteOverview: WebsiteOverview;
logo: string; isDocsTemplate: boolean;
isDocsTemplate?: boolean; isIndexPage: boolean;
categorizedArticles?: { [key: string]: { title: string }[] }; apiUrl: string;
isIndexPage?: boolean;
} = $props(); } = $props();
const categorizedArticles = Object.fromEntries(
Object.entries(
Object.groupBy(
websiteOverview.article.sort((a, b) => (b.article_weight ?? 0) - (a.article_weight ?? 0)),
(article) => article.docs_category?.category_name ?? "Uncategorized"
)
).sort(([a], [b]) =>
a === "Uncategorized"
? 1
: b === "Uncategorized"
? -1
: (websiteOverview.article.find((art) => art.docs_category?.category_name === b)
?.docs_category?.category_weight ?? 0) -
(websiteOverview.article.find((art) => art.docs_category?.category_name === a)
?.docs_category?.category_weight ?? 0)
)
) as { [key: string]: Article[] };
</script> </script>
<nav> <nav>
@@ -53,10 +72,15 @@
</section> </section>
{/if} {/if}
<a href={isIndexPage ? "." : ".."}> <a href={isIndexPage ? "." : ".."}>
{#if logoType === "text"} {#if websiteOverview.header.logo_type === "text"}
<strong>{logo}</strong> <strong>{websiteOverview.header.logo_text}</strong>
{:else} {:else}
<img src={logo} width="24" height="24" alt="" /> <img
src="{apiUrl}/rpc/retrieve_file?id={websiteOverview.header.logo_image}"
width="24"
height="24"
alt=""
/>
{/if} {/if}
</a> </a>
</div> </div>

View File

@@ -2,44 +2,38 @@
import Head from "../common/Head.svelte"; import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte"; import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte"; import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
import type { Article } from "../../db-schema";
const { const {
favicon, websiteOverview,
title, article,
logoType, apiUrl
logo, }: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
mainContent,
categorizedArticles,
footerAdditionalText,
metaDescription
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
categorizedArticles: { [key: string]: { title: string }[] };
footerAdditionalText: string;
metaDescription: string;
} = $props();
</script> </script>
<Head {title} {favicon} nestingLevel={1} {metaDescription} /> <Head
{websiteOverview}
nestingLevel={1}
{apiUrl}
title={article.title}
metaDescription={article.meta_description}
/>
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} isIndexPage={false} /> <Nav {websiteOverview} isDocsTemplate={true} isIndexPage={false} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">
<h1>{title}</h1> <h1>{article.title}</h1>
</div> </div>
</header> </header>
{#if mainContent} {#if article.main_content}
<main> <main>
<div class="container"> <div class="container">
{@html mainContent} {@html md(article.main_content)}
</div> </div>
</main> </main>
{/if} {/if}
<Footer text={footerAdditionalText} isIndexPage={false} /> <Footer {websiteOverview} isIndexPage={false} />

View File

@@ -2,40 +2,39 @@
import Head from "../common/Head.svelte"; import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte"; import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte"; import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
const { const {
favicon, websiteOverview,
title, apiUrl,
logoType, isLegalPage
logo, }: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
mainContent,
categorizedArticles,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
categorizedArticles: { [key: string]: { title: string }[] };
footerAdditionalText: string;
} = $props();
</script> </script>
<Head {title} {favicon} /> <Head
{websiteOverview}
nestingLevel={0}
{apiUrl}
title={isLegalPage ? "Legal information" : websiteOverview.title}
/>
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} /> <Nav {websiteOverview} isDocsTemplate={true} isIndexPage={true} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">
<h1>{title}</h1> <h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
</div> </div>
</header> </header>
<main> <main>
<div class="container"> <div class="container">
{@html mainContent} {@html md(
isLegalPage
? (websiteOverview.legal_information?.main_content ?? "")
: websiteOverview.home.main_content,
false
)}
</div> </div>
</main> </main>
<Footer text={footerAdditionalText} /> <Footer {websiteOverview} isIndexPage={true} />

View File

@@ -5,6 +5,16 @@ import hljs from "highlight.js";
import GithubSlugger from "github-slugger"; import GithubSlugger from "github-slugger";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
import { applyAction, deserialize } from "$app/forms"; import { applyAction, deserialize } from "$app/forms";
import type {
Website,
Settings,
Header,
Home,
Footer,
Article,
DocsCategory,
LegalInformation
} from "$lib/db-schema";
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"]; export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
@@ -189,3 +199,12 @@ export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: s
return ""; return "";
} }
}; };
export interface WebsiteOverview extends Website {
settings: Settings;
header: Header;
home: Home;
footer: Footer;
article: (Article & { docs_category: DocsCategory | null })[];
legal_information?: LegalInformation;
}

View File

@@ -10,7 +10,7 @@ export const actions: Actions = {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
username: data.get("username"), username: data.get("username"),
password: data.get("password") pass: data.get("password")
}) })
}); });

View File

@@ -10,7 +10,7 @@ export const actions: Actions = {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
username: data.get("username"), username: data.get("username"),
password: data.get("password") pass: data.get("password")
}) })
}); });

View File

@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises"; import { rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import type { Website, WebsiteInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => { export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
const searchQuery = url.searchParams.get("website_search_query"); const searchQuery = url.searchParams.get("website_search_query");
@@ -47,7 +48,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
} }
}); });
const websites = await websiteData.json(); const websites: Website[] = await websiteData.json();
return { return {
totalWebsiteCount, totalWebsiteCount,
@@ -66,9 +67,9 @@ export const actions: Actions = {
Authorization: `Bearer ${cookies.get("session_token")}` Authorization: `Bearer ${cookies.get("session_token")}`
}, },
body: JSON.stringify({ body: JSON.stringify({
content_type: data.get("content-type"), content_type: data.get("content-type") as string,
title: data.get("title") title: data.get("title") as string
}) } satisfies WebsiteInput)
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -23,7 +23,7 @@ export const actions: Actions = {
Authorization: `Bearer ${cookies.get("session_token")}` Authorization: `Bearer ${cookies.get("session_token")}`
}, },
body: JSON.stringify({ body: JSON.stringify({
password: data.get("password") pass: data.get("password")
}) })
}); });

View File

@@ -1,6 +1,7 @@
import type { LayoutServerLoad } from "./$types"; import type { LayoutServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import type { Website, Home } from "$lib/db-schema";
export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => { export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
const websiteData = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, { const websiteData = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, {
@@ -25,8 +26,8 @@ export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
} }
}); });
const website = await websiteData.json(); const website: Website = await websiteData.json();
const home = await homeData.json(); const home: Home = await homeData.json();
return { return {
website, website,

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Settings, Header, Footer } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch, cookies }) => { export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
const globalSettingsData = await fetch( const globalSettingsData = await fetch(
@@ -32,9 +33,9 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
} }
}); });
const globalSettings = await globalSettingsData.json(); const globalSettings: Settings = await globalSettingsData.json();
const header = await headerData.json(); const header: Header = await headerData.json();
const footer = await footerData.json(); const footer: Footer = await footerData.json();
return { return {
globalSettings, globalSettings,

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Article, ArticleInput, DocsCategory } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent, locals }) => { export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent, locals }) => {
const searchQuery = url.searchParams.get("article_search_query"); const searchQuery = url.searchParams.get("article_search_query");
@@ -54,7 +55,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent
} }
}); });
const articles = await articlesData.json(); const articles: (Article & { docs_category: DocsCategory | null })[] = await articlesData.json();
return { return {
totalArticleCount, totalArticleCount,
@@ -76,8 +77,8 @@ export const actions: Actions = {
}, },
body: JSON.stringify({ body: JSON.stringify({
website_id: params.websiteId, website_id: params.websiteId,
title: data.get("title") title: data.get("title") as string
}) } satisfies ArticleInput)
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Article, DocsCategory } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => { export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
const articleData = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, { const articleData = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, {
@@ -22,8 +23,8 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
} }
); );
const article = await articleData.json(); const article: Article = await articleData.json();
const categories = await categoryData.json(); const categories: DocsCategory[] = await categoryData.json();
const { website } = await parent(); const { website } = await parent();
return { website, article, categories, API_BASE_PREFIX }; return { website, article, categories, API_BASE_PREFIX };

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import type { DocsCategory, DocsCategoryInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => { export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
const categoryData = await fetch( const categoryData = await fetch(
@@ -13,7 +14,7 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
} }
); );
const categories = await categoryData.json(); const categories: DocsCategory[] = await categoryData.json();
const { website, home } = await parent(); const { website, home } = await parent();
return { return {
@@ -35,9 +36,9 @@ export const actions: Actions = {
}, },
body: JSON.stringify({ body: JSON.stringify({
website_id: params.websiteId, website_id: params.websiteId,
category_name: data.get("category-name"), category_name: data.get("category-name") as string,
category_weight: data.get("category-weight") category_weight: data.get("category-weight") as unknown as number
}) } satisfies DocsCategoryInput)
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Collab, CollabInput, User } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) => { export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) => {
const { website, home } = await parent(); const { website, home } = await parent();
@@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) =
} }
); );
const collaborators = await collabData.json(); const collaborators: (Collab & { user: User })[] = await collabData.json();
return { return {
website, website,
@@ -37,6 +38,8 @@ export const actions: Actions = {
} }
}); });
const user: User = await userData.json();
const res = await fetch(`${API_BASE_PREFIX}/collab`, { const res = await fetch(`${API_BASE_PREFIX}/collab`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -45,9 +48,9 @@ export const actions: Actions = {
}, },
body: JSON.stringify({ body: JSON.stringify({
website_id: params.websiteId, website_id: params.websiteId,
user_id: (await userData.json()).id, user_id: user.id,
permission_level: data.get("permission-level") permission_level: data.get("permission-level") as unknown as number
}) } satisfies CollabInput)
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises"; import { rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import type { LegalInformation, LegalInformationInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => { export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => {
const legalInformationData = await fetch( const legalInformationData = await fetch(
@@ -16,7 +17,7 @@ export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) =
} }
); );
const legalInformation = legalInformationData.ok ? await legalInformationData.json() : null; const legalInformation: LegalInformation = await legalInformationData.json();
const { website } = await parent(); const { website } = await parent();
return { return {
@@ -39,8 +40,8 @@ export const actions: Actions = {
}, },
body: JSON.stringify({ body: JSON.stringify({
website_id: params.websiteId, website_id: params.websiteId,
main_content: data.get("main-content") main_content: data.get("main-content") as string
}) } satisfies LegalInformationInput)
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -8,7 +8,7 @@
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let previewContent = $state(data.legalInformation?.main_content); let previewContent = $state(data.legalInformation.main_content);
let mainContentTextarea: HTMLTextAreaElement; let mainContentTextarea: HTMLTextAreaElement;
let textareaScrollTop = $state(0); let textareaScrollTop = $state(0);
@@ -80,14 +80,14 @@
bind:value={previewContent} bind:value={previewContent}
bind:this={mainContentTextarea} bind:this={mainContentTextarea}
onscroll={updateScrollPercentage} onscroll={updateScrollPercentage}
required>{data.legalInformation?.main_content ?? ""}</textarea required>{data.legalInformation.main_content ?? ""}</textarea
> >
</label> </label>
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
{#if data.legalInformation?.main_content} {#if data.legalInformation.main_content}
<Modal id="delete-legal-information" text="Delete"> <Modal id="delete-legal-information" text="Delete">
<form <form
action="?/deleteLegalInformation" action="?/deleteLegalInformation"
@@ -98,7 +98,6 @@
await update(); await update();
window.location.hash = "!"; window.location.hash = "!";
sending = false; sending = false;
previewContent = null;
}; };
}} }}
> >

View File

@@ -1,6 +1,6 @@
import { readFile, mkdir, writeFile } from "node:fs/promises"; import { readFile, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { md } from "$lib/utils"; import { type WebsiteOverview } from "$lib/utils";
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { render } from "svelte/server"; import { render } from "svelte/server";
@@ -10,34 +10,9 @@ import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte"; import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import { dev } from "$app/environment"; import { dev } from "$app/environment";
interface WebsiteData { export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
id: string;
content_type: "Blog" | "Docs";
favicon_image: string | null;
title: string;
logo_type: "text" | "image";
logo_text: string | null;
logo_image: string | null;
main_content: string;
additional_text: string;
accent_color_light_theme: string;
accent_color_dark_theme: string;
articles: {
cover_image: string | null;
title: string;
publication_date: string;
meta_description: string;
main_content: string;
}[];
categorized_articles: {
[key: string]: { title: string; publication_date: string; meta_description: string }[];
};
legal_information_main_content: string | null;
}
export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) => {
const websiteOverviewData = await fetch( const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`, `${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -48,8 +23,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
} }
); );
const websiteOverview = await websiteOverviewData.json(); const websiteOverview: WebsiteOverview = await websiteOverviewData.json();
const { website } = await parent();
generateStaticFiles(websiteOverview); generateStaticFiles(websiteOverview);
@@ -70,15 +44,14 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
return { return {
websiteOverview, websiteOverview,
websitePreviewUrl, websitePreviewUrl,
websiteProdUrl, websiteProdUrl
website
}; };
}; };
export const actions: Actions = { export const actions: Actions = {
publishWebsite: async ({ fetch, params, cookies }) => { publishWebsite: async ({ fetch, params, cookies }) => {
const websiteOverviewData = await fetch( const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`, `${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -112,54 +85,9 @@ export const actions: Actions = {
} }
}; };
const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean = true) => { const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: boolean = true) => {
let head = ""; const fileContents = (head: string, body: string) => {
let body = ""; return `
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: websiteData.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.main_content ?? "", false),
articles: websiteData.articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: websiteData.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.main_content ?? "", false),
categorizedArticles: websiteData.categorized_articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
}
const indexFileContents = `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -169,6 +97,15 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
${body} ${body}
</body> </body>
</html>`; </html>`;
};
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
props: {
websiteOverview: websiteData,
apiUrl: API_BASE_PREFIX,
isLegalPage: false
}
});
let uploadDir = ""; let uploadDir = "";
@@ -179,138 +116,38 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
} }
await mkdir(uploadDir, { recursive: true }); await mkdir(uploadDir, { recursive: true });
await writeFile(join(uploadDir, "index.html"), indexFileContents); await writeFile(join(uploadDir, "index.html"), fileContents(head, body));
await mkdir(join(uploadDir, "articles"), { await mkdir(join(uploadDir, "articles"), {
recursive: true recursive: true
}); });
for (const article of websiteData.articles ?? []) { for (const article of websiteData.article ?? []) {
const articleFileName = article.title.toLowerCase().split(" ").join("-"); const articleFileName = article.title.toLowerCase().split(" ").join("-");
let head = ""; const { head, body } = render(websiteData.content_type === "Blog" ? BlogArticle : DocsArticle, {
let body = "";
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogArticle, {
props: { props: {
favicon: websiteData.favicon_image websiteOverview: websiteData,
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}` article,
: "", apiUrl: API_BASE_PREFIX
title: article.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
coverImage: article.cover_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}`
: "",
publicationDate: article.publication_date,
mainContent: md(article.main_content ?? ""),
footerAdditionalText: md(websiteData.additional_text ?? ""),
metaDescription: article.meta_description
} }
})); });
await writeFile(
join(uploadDir, "articles", `${articleFileName}.html`),
fileContents(head, body)
);
} }
break;
case "Docs": if (websiteData.legal_information) {
{ const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
({ head, body } = render(DocsArticle, {
props: { props: {
favicon: websiteData.favicon_image websiteOverview: websiteData,
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}` apiUrl: API_BASE_PREFIX,
: "", isLegalPage: true
title: article.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(article.main_content ?? ""),
categorizedArticles: websiteData.categorized_articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? ""),
metaDescription: article.meta_description
}
}));
}
break;
} }
});
const articleFileContents = ` await writeFile(join(uploadDir, "legal-information.html"), fileContents(head, body));
<!DOCTYPE html>
<html lang="en">
<head>
${head}
</head>
<body>
${body}
</body>
</html>`;
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
}
if (websiteData.legal_information_main_content) {
let head = "";
let body = "";
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
articles: [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
categorizedArticles: {},
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
}
const legalInformationFileContents = `
<!DOCTYPE html>
<html lang="en">
<head>
${head}
</head>
<body>
${body}
</body>
</html>`;
await writeFile(join(uploadDir, "legal-information.html"), legalInformationFileContents);
} }
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, { const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
@@ -328,14 +165,14 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
.concat(specificStyles) .concat(specificStyles)
.replace( .replace(
/--color-accent:\s*(.*?);/, /--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.accent_color_dark_theme};` `--color-accent: ${websiteData.settings.accent_color_dark_theme};`
) )
.replace( .replace(
/@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/, /@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/,
(match) => (match) =>
match.replace( match.replace(
/--color-accent:\s*(.*?);/, /--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.accent_color_light_theme};` `--color-accent: ${websiteData.settings.accent_color_light_theme};`
) )
) )
); );

View File

@@ -17,9 +17,9 @@
{/if} {/if}
<WebsiteEditor <WebsiteEditor
id={data.website.id} id={data.websiteOverview.id}
contentType={data.website.content_type} contentType={data.websiteOverview.content_type}
title={data.website.title} title={data.websiteOverview.title}
previewContent={data.websitePreviewUrl} previewContent={data.websitePreviewUrl}
fullPreview={true} fullPreview={true}
> >
@@ -46,7 +46,7 @@
<button type="submit">Publish</button> <button type="submit">Publish</button>
</form> </form>
{#if data.website.is_published} {#if data.websiteOverview.is_published}
<section id="publication-status"> <section id="publication-status">
<h3> <h3>
<a href="#publication-status">Publication status</a> <a href="#publication-status">Publication status</a>

View File

@@ -25,11 +25,12 @@
--color-text: black; --color-text: black;
--color-text-invert: white; --color-text-invert: white;
--color-border: hsl(0 0% 50%);
--color-accent: hsl(210, 100%, 30%); --color-accent: hsl(210, 100%, 30%);
--color-success: hsl(105, 100%, 30%); --color-success: hsl(105, 100%, 30%);
--color-error: hsl(0, 100%, 30%); --color-error: hsl(0, 100%, 30%);
--border-primary: 0.0625rem solid var(--bg-tertiary); --border-primary: 0.0625rem solid var(--color-border);
--border-radius: 0.125rem; --border-radius: 0.125rem;
/* Step -1: 14.9953px → 14.2222px */ /* Step -1: 14.9953px → 14.2222px */
@@ -222,6 +223,10 @@ a {
color: var(--color-accent); color: var(--color-accent);
} }
a:has(img) {
display: inline-block;
}
:is(h1, h2, h3, h4, h5, h6) > a { :is(h1, h2, h3, h4, h5, h6) > a {
color: var(--color-text); color: var(--color-text);
text-decoration: none; text-decoration: none;
@@ -242,6 +247,7 @@ svg,
video { video {
max-inline-size: 100%; max-inline-size: 100%;
block-size: auto; block-size: auto;
vertical-align: middle;
} }
p, p,