diff --git a/src/deploy/enum_friendship_status.sql b/src/deploy/enum_friendship_status.sql deleted file mode 100644 index fa585a6c..00000000 --- a/src/deploy/enum_friendship_status.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - -COMMIT; diff --git a/src/deploy/function_friendship.sql b/src/deploy/function_friendship.sql new file mode 100644 index 00000000..8b326272 --- /dev/null +++ b/src/deploy/function_friendship.sql @@ -0,0 +1,180 @@ +BEGIN; + +-- accept friendship request + +CREATE FUNCTION vibetype.friendship_accept( + requestor_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _friend_account_id UUID; + _id UUID; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; + + IF _id IS NULL THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_accept(UUID) IS E'Accepts a friendship request.\n\nError codes:\n- **VTFAC** when a corresponding friendship request does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_accept(UUID) TO vibetype_account; + +-- cancel friendship + +CREATE FUNCTION vibetype.friendship_cancel( + friend_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_cancel(UUID) IS 'Cancels a friendship (in both directions) if it exists.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_cancel(UUID) TO vibetype_account; + +-- reject friendship request + +CREATE FUNCTION vibetype.friendship_reject( + requestor_account_id UUID +) RETURNS VOID AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_reject(UUID) IS 'Rejects a friendship request'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_reject(UUID) TO vibetype_account; + +-- request friendship + +CREATE FUNCTION vibetype.friendship_request( + friend_account_id UUID +) RETURNS VOID AS $$ +DECLARE + _account_id UUID; + _language TEXT; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF _account_id IN (SELECT id FROM vibetype_private.account_block_ids()) + OR friend_account_id IN (SELECT id FROM vibetype_private.account_block_ids()) THEN + RETURN; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); + + SELECT COALESCE(language::TEXT, 'de') INTO _language + FROM vibetype.contact + WHERE account_id = _account_id AND created_by = _account_id; + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_request.friend_account_id + ), + 'template', jsonb_build_object('language', _language) + )) + ); + +END; $$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION vibetype.friendship_request(UUID) IS E'Starts a new friendship request.\n\nError codes:\n- **VTFEX** when the friendship already exists.\n- **VTREQ** when there is already a friendship request.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_request(UUID) TO vibetype_account; + + +-- toggle closeness of friendship + +CREATE FUNCTION vibetype.friendship_toggle_closeness( + friend_account_id UUID +) RETURNS BOOLEAN AS $$ +DECLARE + _account_id UUID; + _result BOOLEAN; + _is_close_friend BOOLEAN; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT TRUE + INTO _result + FROM vibetype.friendship f + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF _result IS NULL THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship_closeness f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$ LANGUAGE plpgsql SECURITY INVOKER; + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(UUID) IS E'Toggles a friendship relation between ''not a close friend'' and ''close friend''.\n\nError codes:\n- **VTFTC** when the friendship does not exist.'; + +GRANT EXECUTE ON FUNCTION vibetype.friendship_toggle_closeness(UUID) TO vibetype_account; + +COMMIT; diff --git a/src/deploy/table_friendship.sql b/src/deploy/table_friendship.sql index 4744ce89..2d824307 100644 --- a/src/deploy/table_friendship.sql +++ b/src/deploy/table_friendship.sql @@ -1,79 +1,197 @@ BEGIN; +----------------------------------------------------------- +-- TABLE vibetype.friendship_request +----------------------------------------------------------- + +CREATE TABLE vibetype.friendship_request ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) +); + +GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship_request TO vibetype_account; + +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; + +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + +-- Only allow interactions with friendships in which the current user is involved. +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT +USING ( + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() +); + +-- Only allow creation by the current user. +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT +WITH CHECK ( + created_by = vibetype.invoker_account_id() +); + +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE +USING ( + friend_account_id = vibetype.invoker_account_id() +); + +----------------------------------------------------------- +-- TABLE vibetype.friendship +----------------------------------------------------------- + CREATE TABLE vibetype.friendship ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - a_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - b_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - status vibetype.friendship_status NOT NULL DEFAULT 'requested'::vibetype.friendship_status, + account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + friend_account_id UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, - updated_at TIMESTAMP WITH TIME ZONE, - updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, - UNIQUE (a_account_id, b_account_id), - CONSTRAINT friendship_creator_participant CHECK (created_by = a_account_id or created_by = b_account_id), - CONSTRAINT friendship_creator_updater_difference CHECK (created_by <> updated_by), - CONSTRAINT friendship_ordering CHECK (a_account_id < b_account_id), - CONSTRAINT friendship_updater_participant CHECK (updated_by IS NULL or updated_by = a_account_id or updated_by = b_account_id) + UNIQUE (account_id, friend_account_id), + CONSTRAINT friendship_creator_friend CHECK (account_id <> friend_account_id), + CONSTRAINT friendship_creator_participant CHECK (created_by = account_id) ); CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); -CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updated_by); COMMENT ON TABLE vibetype.friendship IS 'A friend relation together with its status.'; COMMENT ON COLUMN vibetype.friendship.id IS E'@omit create,update\nThe friend relation''s internal id.'; -COMMENT ON COLUMN vibetype.friendship.a_account_id IS E'@omit update\nThe ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; -COMMENT ON COLUMN vibetype.friendship.b_account_id IS E'@omit update\nThe ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; -COMMENT ON COLUMN vibetype.friendship.status IS E'@omit create\nThe status of the friend relation.'; +COMMENT ON COLUMN vibetype.friendship.account_id IS E'@omit update\nOne side of the friend relation.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS E'@omit update\nThe other side of the friend relation.'; COMMENT ON COLUMN vibetype.friendship.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; COMMENT ON COLUMN vibetype.friendship.created_by IS E'@omit update\nThe account that created the friend relation was created.'; -COMMENT ON COLUMN vibetype.friendship.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s status was updated.'; -COMMENT ON COLUMN vibetype.friendship.updated_by IS E'@omit create,update\nThe account that updated the friend relation''s status.'; COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; -COMMENT ON INDEX vibetype.idx_friendship_updated_by IS 'B-Tree index to optimize lookups by updater.'; - -CREATE TRIGGER vibetype_trigger_friendship_update - BEFORE - UPDATE - ON vibetype.friendship - FOR EACH ROW - EXECUTE PROCEDURE vibetype.trigger_metadata_update(); -GRANT INSERT, SELECT, UPDATE, DELETE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT, INSERT, DELETE ON TABLE vibetype.friendship TO vibetype_account; ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + -- Only allow interactions with friendships in which the current user is involved. -CREATE POLICY friendship_existing ON vibetype.friendship FOR ALL +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING ( - ( - vibetype.invoker_account_id() = a_account_id - AND b_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + TRUE +); + +-- Only allow creation by the current user and only if a friendship request is present. +CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT +WITH CHECK ( + (account_id, friend_account_id, created_by) IN ( + SELECT account_id, friend_account_id, account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() ) OR - ( - vibetype.invoker_account_id() = b_account_id - AND a_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + (account_id, friend_account_id, created_by) IN ( + SELECT friend_account_id, account_id, friend_account_id + FROM vibetype.friendship_request + WHERE friend_account_id = vibetype.invoker_account_id() ) -) -WITH CHECK (FALSE); +); --- Only allow creation by the current user. -CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT +-- Only allow deletion if the current user is involved in the friendship. +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE +USING ( + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() +); + +----------------------------------------------------------- +-- TABLE vibetype.friendship_closeness +----------------------------------------------------------- + +CREATE TABLE vibetype.friendship_closeness ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + account_id UUID NOT NULL, + friend_account_id UUID NOT NULL, + + is_close_friend BOOLEAN NOT NULL DEFAuLT FALSE, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE, + updated_at TIMESTAMP WITH TIME ZONE, + updated_by UUID REFERENCES vibetype.account(id) ON DELETE SET NULL, + + UNIQUE (account_id, friend_account_id), + CONSTRAINT fk_friendship_closeness FOREIGN KEY (account_id, friend_account_id) REFERENCES vibetype.friendship(account_id, friend_account_id) ON DELETE CASCADE, + CONSTRAINT friendship_closeness_creator CHECK (created_by = account_id), + CONSTRAINT friendship_closeness_updater CHECK (updated_by = account_id) +); + +CREATE INDEX idx_friendship_closeness_created_by ON vibetype.friendship_closeness USING btree (created_by); +CREATE INDEX idx_friendship_closeness_updated_by ON vibetype.friendship_closeness USING btree (updated_by); + +COMMENT ON TABLE vibetype.friendship_closeness IS 'The presence of a row in this tables indicates that account_id considers friend_account_id as a close friend.'; +COMMENT ON COLUMN vibetype.friendship_closeness.id IS E'@omit create,update\nThe friend relation''s internal id.'; +COMMENT ON COLUMN vibetype.friendship_closeness.account_id IS E'@omit update\nThe one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; +COMMENT ON COLUMN vibetype.friendship_closeness.friend_account_id IS E'@omit update\nThe other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; +COMMENT ON COLUMN vibetype.friendship_closeness.is_close_friend IS E'@omit create\nThe flag indicating whether account_id considers friend_account_id as a close friend or not.'; +COMMENT ON COLUMN vibetype.friendship_closeness.created_at IS E'@omit create,update\nThe timestamp when the friend relation was created.'; +COMMENT ON COLUMN vibetype.friendship_closeness.created_by IS E'@omit update\nThe account that created the friend relation was created.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_at IS E'@omit create,update\nThe timestamp when the friend relation''s closeness status was updated.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_by IS E'@omit create,update\nThe account that updated the friend relation''s closeness status.'; + +CREATE TRIGGER vibetype_trigger_friendship_closeness_update + BEFORE + UPDATE + ON vibetype.friendship_closeness + FOR EACH ROW + EXECUTE PROCEDURE vibetype.trigger_metadata_update(); + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE vibetype.friendship_closeness TO vibetype_account; + +ALTER TABLE vibetype.friendship_closeness ENABLE ROW LEVEL SECURITY; + +CREATE POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness AS RESTRICTIVE FOR ALL +USING ( + account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) + AND friend_account_id NOT IN (SELECT id FROM vibetype_private.account_block_ids()) +); + +-- Only allow selection of close friends of the current account. +CREATE POLICY friendship_closeness_select ON vibetype.friendship_closeness FOR SELECT +USING ( + account_id = vibetype.invoker_account_id() +); + +-- Only allow creation by the current user and only if a friendship request is present. +CREATE POLICY friendship_closeness_insert ON vibetype.friendship_closeness FOR INSERT WITH CHECK ( - created_by = vibetype.invoker_account_id() + account_id = vibetype.invoker_account_id() + OR + friend_account_id = vibetype.invoker_account_id() ); --- Only allow update by the current user and only the state transition requested -> accepted. -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE +CREATE POLICY friendship_closeness_update ON vibetype.friendship_closeness FOR UPDATE USING ( - status = 'requested'::vibetype.friendship_status -) WITH CHECK ( - status = 'accepted'::vibetype.friendship_status - AND + account_id = vibetype.invoker_account_id() +) +WITH CHECK ( updated_by = vibetype.invoker_account_id() ); +-- Only allow deletion if the current user is involved in the friendship. +CREATE POLICY friendship_closeness_delete ON vibetype.friendship_closeness FOR DELETE +USING ( + account_id = vibetype.invoker_account_id() +); + COMMIT; diff --git a/src/revert/enum_friendship_status.sql b/src/revert/enum_friendship_status.sql deleted file mode 100644 index 04e22d38..00000000 --- a/src/revert/enum_friendship_status.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN; - -DROP TYPE vibetype.friendship_status; - -COMMIT; diff --git a/src/revert/function_friendship.sql b/src/revert/function_friendship.sql new file mode 100644 index 00000000..a1efcfe9 --- /dev/null +++ b/src/revert/function_friendship.sql @@ -0,0 +1,9 @@ +BEGIN; + +DROP FUNCTION vibetype.friendship_accept(UUID); +DROP FUNCTION vibetype.friendship_cancel(UUID); +DROP FUNCTION vibetype.friendship_reject(UUID); +DROP FUNCTION vibetype.friendship_request(UUID); +DROP FUNCTION vibetype.friendship_toggle_closeness(UUID); + +COMMIT; diff --git a/src/revert/table_friendship.sql b/src/revert/table_friendship.sql index 8d3a165d..f717fdb6 100644 --- a/src/revert/table_friendship.sql +++ b/src/revert/table_friendship.sql @@ -1,13 +1,38 @@ BEGIN; -DROP POLICY friendship_update ON vibetype.friendship; -DROP POLICY friendship_insert ON vibetype.friendship; -DROP POLICY friendship_existing ON vibetype.friendship; +-- vibetype.friendship_closeness + +DROP POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_select ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_insert ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_update ON vibetype.friendship_closeness; +DROP POLICY friendship_closeness_delete ON vibetype.friendship_closeness; + +DROP INDEX vibetype.idx_friendship_closeness_updated_by; +DROP INDEX vibetype.idx_friendship_closeness_created_by; -DROP TRIGGER vibetype_trigger_friendship_update ON vibetype.friendship; +DROP TRIGGER vibetype_trigger_friendship_closeness_update ON vibetype.friendship_closeness; + +DROP TABLE vibetype.friendship_closeness; + +-- vibetype.friendship + +DROP POLICY friendship_not_blocked ON vibetype.friendship; +DROP POLICY friendship_select ON vibetype.friendship; +DROP POLICY friendship_insert ON vibetype.friendship; +DROP POLICY friendship_delete ON vibetype.friendship; -DROP INDEX vibetype.idx_friendship_updated_by; DROP INDEX vibetype.idx_friendship_created_by; + DROP TABLE vibetype.friendship; +-- vibetype.friendship_request + +DROP POLICY friendship_request_not_blocked ON vibetype.friendship_request; +DROP POLICY friendship_request_select ON vibetype.friendship_request; +DROP POLICY friendship_request_insert ON vibetype.friendship_request; +DROP POLICY friendship_request_delete ON vibetype.friendship_request; + +DROP TABLE vibetype.friendship_request; + COMMIT; diff --git a/src/sqitch.plan b/src/sqitch.plan index fd0dbbf2..6cac1bd3 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -82,8 +82,7 @@ table_event_favorite [schema_public table_account_public table_event] 1970-01-01 function_guest_create_multiple [schema_public table_guest role_account] 1970-01-01T00:00:00Z Sven Thelemann # Function for inserting multiple guest records. function_event_search [privilege_execute_revoke schema_public enum_language schema_private function_language_iso_full_text_search table_event role_account role_anonymous] 1970-01-01T00:00:00Z Jonas Thelemann # Full-text search on events. table_device [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Jonas Thelemann # A device that's assigned to an account. -enum_friendship_status [schema_public] 1970-01-01T00:00:00Z Sven Thelemann # Possible status values of a friend relation. -table_friendship [schema_public enum_friendship_status table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. +table_friendship [schema_public table_account_public function_trigger_metadata_update] 1970-01-01T00:00:00Z Sven Thelemann # A friend relation together with its status. table_event_format [schema_public role_anonymous role_account] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event formats. table_event_format_mapping [schema_public table_event table_event_format role_anonymous role_account function_invoker_account_id] 1970-01-01T00:00:00Z Jonas Thelemann # Table for storing event to category (M:N) relationships. table_audit_log [schema_private] 1970-01-01T00:00:00Z Sven Thelemann # Table for storing audit log records. @@ -95,3 +94,4 @@ table_preference_event_location [schema_public table_account_public role_account role_zammad 1970-01-01T00:00:00Z Sven Thelemann # Add role zammad. database_zammad [role_zammad] 1970-01-01T00:00:00Z Sven Thelemann # Add the database for zammad. function_account_search [schema_public table_account_public role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add a function for searching accounts based on a substring query. +function_friendship [schema_public table_friendship role_account] 1970-01-01T00:00:00Z Svens Thelemann # Add functions for handling friendships. diff --git a/src/verify/enum_friendship_status.sql b/src/verify/enum_friendship_status.sql deleted file mode 100644 index 8bbb77c5..00000000 --- a/src/verify/enum_friendship_status.sql +++ /dev/null @@ -1,8 +0,0 @@ -BEGIN; - -DO $$ -BEGIN - ASSERT (SELECT pg_catalog.has_type_privilege('vibetype.friendship_status', 'USAGE')); -END $$; - -ROLLBACK; diff --git a/src/verify/function_friendship.sql b/src/verify/function_friendship.sql new file mode 100644 index 00000000..24079f4a --- /dev/null +++ b/src/verify/function_friendship.sql @@ -0,0 +1,28 @@ +BEGIN; + +DO $$ +BEGIN + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_accept(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_accept(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_cancel(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_cancel(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_reject(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_reject(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_request(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_request(UUID).'; + END IF; + + IF NOT (SELECT pg_catalog.has_function_privilege('vibetype_account', 'vibetype.friendship_toggle_closeness(UUID)', 'EXECUTE')) THEN + RAISE EXCEPTION 'Test failed: vibetype_account does not have EXECUTE privilege for vibetype.friendship_toggle_closeness(UUID).'; + END IF; + +END $$; + +ROLLBACK; diff --git a/src/verify/table_friendship.sql b/src/verify/table_friendship.sql index 66005c24..f6fd07d4 100644 --- a/src/verify/table_friendship.sql +++ b/src/verify/table_friendship.sql @@ -2,14 +2,32 @@ BEGIN; SELECT id, - a_account_id, - b_account_id, - status, + account_id, + friend_account_id, + created_at, + created_by +FROM vibetype.friendship_request +WHERE FALSE; + +SELECT + id, + account_id, + friend_account_id, + created_at, + created_by +FROM vibetype.friendship +WHERE FALSE; + +SELECT + id, + account_id, + friend_account_id, + is_close_friend, created_at, created_by, updated_at, updated_by -FROM vibetype.friendship +FROM vibetype.friendship_closeness WHERE FALSE; ROLLBACK; @@ -23,7 +41,7 @@ DO $$ BEGIN ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'SELECT')); ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'INSERT')); - ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'UPDATE')); ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship', 'DELETE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship', 'SELECT')); ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship', 'INSERT')); @@ -33,6 +51,33 @@ BEGIN ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'INSERT')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'UPDATE')); ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship', 'DELETE')); + + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'SELECT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'UPDATE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_request', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_request', 'DELETE')); + + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'SELECT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'INSERT')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.friendship_closeness', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.friendship_closeness', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege(current_setting('role.vibetype_username'), 'vibetype.friendship_closeness', 'DELETE')); + END $$; ROLLBACK; diff --git a/test/fixture/schema_vibetype.definition.sql b/test/fixture/schema_vibetype.definition.sql index 90a680d7..916195da 100644 --- a/test/fixture/schema_vibetype.definition.sql +++ b/test/fixture/schema_vibetype.definition.sql @@ -136,26 +136,6 @@ ALTER TYPE vibetype.event_visibility OWNER TO ci; COMMENT ON TYPE vibetype.event_visibility IS 'Possible visibilities of events and event groups: public, private and unlisted.'; --- --- Name: friendship_status; Type: TYPE; Schema: vibetype; Owner: ci --- - -CREATE TYPE vibetype.friendship_status AS ENUM ( - 'accepted', - 'requested' -); - - -ALTER TYPE vibetype.friendship_status OWNER TO ci; - --- --- Name: TYPE friendship_status; Type: COMMENT; Schema: vibetype; Owner: ci --- - -COMMENT ON TYPE vibetype.friendship_status IS 'Possible status values of a friend relation. -There is no status `rejected` because friendship records will be deleted when a friendship request is rejected.'; - - -- -- Name: invitation_feedback; Type: TYPE; Schema: vibetype; Owner: ci -- @@ -1310,6 +1290,232 @@ ALTER FUNCTION vibetype.events_organized() OWNER TO ci; COMMENT ON FUNCTION vibetype.events_organized() IS 'Add a function that returns all event ids for which the invoker is the creator.'; +-- +-- Name: friendship_accept(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_accept(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _friend_account_id UUID; + _id UUID; +BEGIN + + _friend_account_id := vibetype.invoker_account_id(); + + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = _friend_account_id; + + IF _id IS NULL THEN + RAISE EXCEPTION 'Friendship request does not exist' USING ERRCODE = 'VTFAC'; + END IF; + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (requestor_account_id, _friend_account_id, requestor_account_id); + + INSERT INTO vibetype.friendship_closeness(account_id, friend_account_id, created_by) + VALUES (_friend_account_id, requestor_account_id, _friend_account_id); + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_accept(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) IS 'Accepts a friendship request. + +Error codes: +- **VTFAC** when a corresponding friendship request does not exist.'; + + +-- +-- Name: friendship_cancel(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_cancel(friend_account_id uuid) RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + DELETE FROM vibetype.friendship f + WHERE (account_id = _account_id AND f.friend_account_id = friendship_cancel.friend_account_id) + OR (account_id = friendship_cancel.friend_account_id AND f.friend_account_id = _account_id); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_cancel(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) IS 'Cancels a friendship (in both directions) if it exists.'; + + +-- +-- Name: friendship_reject(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_reject(requestor_account_id uuid) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +BEGIN + + DELETE FROM vibetype.friendship_request + WHERE account_id = requestor_account_id AND friend_account_id = vibetype.invoker_account_id(); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_reject(requestor_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) IS 'Rejects a friendship request'; + + +-- +-- Name: friendship_request(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_request(friend_account_id uuid) RETURNS void + LANGUAGE plpgsql SECURITY DEFINER + AS $$ +DECLARE + _account_id UUID; + _language TEXT; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + IF _account_id IN (SELECT id FROM vibetype_private.account_block_ids()) + OR friend_account_id IN (SELECT id FROM vibetype_private.account_block_ids()) THEN + RETURN; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship f + WHERE (f.account_id = _account_id AND f.friend_account_id = friendship_request.friend_account_id) + ) + THEN + RAISE EXCEPTION 'Friendship already exists.' USING ERRCODE = 'VTFEX'; + END IF; + + IF EXISTS( + SELECT 1 + FROM vibetype.friendship_request r + WHERE (r.account_id = _account_id AND r.friend_account_id = friendship_request.friend_account_id) + OR (r.account_id = friendship_request.friend_account_id AND r.friend_account_id = _account_id) + ) + THEN + RAISE EXCEPTION 'There is already a friendship request.' USING ERRCODE = 'VTREQ'; + END IF; + + INSERT INTO vibetype.friendship_request(account_id, friend_account_id, created_by) + VALUES (_account_id, friendship_request.friend_account_id, _account_id); + + SELECT COALESCE(language::TEXT, 'de') INTO _language + FROM vibetype.contact + WHERE account_id = _account_id AND created_by = _account_id; + + INSERT INTO vibetype_private.notification (channel, payload) + VALUES ( + 'friendship_request', + jsonb_pretty(jsonb_build_object( + 'data', jsonb_build_object( + 'requestor_account_id', vibetype.invoker_account_id(), + 'requestee_account_id', friendship_request.friend_account_id + ), + 'template', jsonb_build_object('language', _language) + )) + ); + +END; $$; + + +ALTER FUNCTION vibetype.friendship_request(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_request(friend_account_id uuid) IS 'Starts a new friendship request. + +Error codes: +- **VTFEX** when the friendship already exists. +- **VTREQ** when there is already a friendship request.'; + + +-- +-- Name: friendship_toggle_closeness(uuid); Type: FUNCTION; Schema: vibetype; Owner: ci +-- + +CREATE FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) RETURNS boolean + LANGUAGE plpgsql + AS $$ +DECLARE + _account_id UUID; + _result BOOLEAN; + _is_close_friend BOOLEAN; +BEGIN + + _account_id := vibetype.invoker_account_id(); + + SELECT TRUE + INTO _result + FROM vibetype.friendship f + WHERE f.account_id = _account_id + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id; + + IF _result IS NULL THEN + RAISE EXCEPTION 'Friendship does not exist' USING ERRCODE = 'VTFTC'; + END IF; + + UPDATE vibetype.friendship_closeness f + SET is_close_friend = NOT is_close_friend + WHERE account_id = vibetype.invoker_account_id() + AND f.friend_account_id = friendship_toggle_closeness.friend_account_id + RETURNING is_close_friend INTO _is_close_friend; + + RETURN _is_close_friend; + +END; $$; + + +ALTER FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) OWNER TO ci; + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) IS 'Toggles a friendship relation between ''not a close friend'' and ''close friend''. + +Error codes: +- **VTFTC** when the friendship does not exist.'; + + -- -- Name: guest_claim_array(); Type: FUNCTION; Schema: vibetype; Owner: ci -- @@ -3313,17 +3519,12 @@ Reference to the uploaded file.'; CREATE TABLE vibetype.friendship ( id uuid DEFAULT gen_random_uuid() NOT NULL, - a_account_id uuid NOT NULL, - b_account_id uuid NOT NULL, - status vibetype.friendship_status DEFAULT 'requested'::vibetype.friendship_status NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by uuid NOT NULL, - updated_at timestamp with time zone, - updated_by uuid, - CONSTRAINT friendship_creator_participant CHECK (((created_by = a_account_id) OR (created_by = b_account_id))), - CONSTRAINT friendship_creator_updater_difference CHECK ((created_by <> updated_by)), - CONSTRAINT friendship_ordering CHECK ((a_account_id < b_account_id)), - CONSTRAINT friendship_updater_participant CHECK (((updated_by IS NULL) OR (updated_by = a_account_id) OR (updated_by = b_account_id))) + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) ); @@ -3345,61 +3546,145 @@ The friend relation''s internal id.'; -- --- Name: COLUMN friendship.a_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.account_id; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.a_account_id IS '@omit update -The ''left'' side of the friend relation. It must be lexically less than the ''right'' side.'; +COMMENT ON COLUMN vibetype.friendship.account_id IS '@omit update +One side of the friend relation.'; -- --- Name: COLUMN friendship.b_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.friend_account_id; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.b_account_id IS '@omit update -The ''right'' side of the friend relation. It must be lexically greater than the ''left'' side.'; +COMMENT ON COLUMN vibetype.friendship.friend_account_id IS '@omit update +The other side of the friend relation.'; -- --- Name: COLUMN friendship.status; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.status IS '@omit create -The status of the friend relation.'; +COMMENT ON COLUMN vibetype.friendship.created_at IS '@omit create,update +The timestamp when the friend relation was created.'; -- --- Name: COLUMN friendship.created_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship.created_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.created_at IS '@omit create,update +COMMENT ON COLUMN vibetype.friendship.created_by IS '@omit update +The account that created the friend relation was created.'; + + +-- +-- Name: friendship_closeness; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.friendship_closeness ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + is_close_friend boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + updated_at timestamp with time zone, + updated_by uuid, + CONSTRAINT friendship_closeness_creator CHECK ((created_by = account_id)), + CONSTRAINT friendship_closeness_updater CHECK ((updated_by = account_id)) +); + + +ALTER TABLE vibetype.friendship_closeness OWNER TO ci; + +-- +-- Name: TABLE friendship_closeness; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON TABLE vibetype.friendship_closeness IS 'The presence of a row in this tables indicates that account_id considers friend_account_id as a close friend.'; + + +-- +-- Name: COLUMN friendship_closeness.id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.id IS '@omit create,update +The friend relation''s internal id.'; + + +-- +-- Name: COLUMN friendship_closeness.account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.account_id IS '@omit update +The one side of the friend relation. If the status is ''requested'' then it is the requestor account.'; + + +-- +-- Name: COLUMN friendship_closeness.friend_account_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.friend_account_id IS '@omit update +The other side of the friend relation. If the status is ''requested'' then it is the requestee account.'; + + +-- +-- Name: COLUMN friendship_closeness.is_close_friend; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.is_close_friend IS '@omit create +The flag indicating whether account_id considers friend_account_id as a close friend or not.'; + + +-- +-- Name: COLUMN friendship_closeness.created_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.friendship_closeness.created_at IS '@omit create,update The timestamp when the friend relation was created.'; -- --- Name: COLUMN friendship.created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.created_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.created_by IS '@omit update +COMMENT ON COLUMN vibetype.friendship_closeness.created_by IS '@omit update The account that created the friend relation was created.'; -- --- Name: COLUMN friendship.updated_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.updated_at; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.updated_at IS '@omit create,update -The timestamp when the friend relation''s status was updated.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_at IS '@omit create,update +The timestamp when the friend relation''s closeness status was updated.'; -- --- Name: COLUMN friendship.updated_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: COLUMN friendship_closeness.updated_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype.friendship.updated_by IS '@omit create,update -The account that updated the friend relation''s status.'; +COMMENT ON COLUMN vibetype.friendship_closeness.updated_by IS '@omit create,update +The account that updated the friend relation''s closeness status.'; +-- +-- Name: friendship_request; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.friendship_request ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + account_id uuid NOT NULL, + friend_account_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + CONSTRAINT friendship_creator_friend CHECK ((account_id <> friend_account_id)), + CONSTRAINT friendship_creator_participant CHECK ((created_by = account_id)) +); + + +ALTER TABLE vibetype.friendship_request OWNER TO ci; + -- -- Name: guest_flat; Type: VIEW; Schema: vibetype; Owner: ci -- @@ -4604,11 +4889,27 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_b_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_b_account_id_key UNIQUE (a_account_id, b_account_id); + ADD CONSTRAINT friendship_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_closeness friendship_closeness_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_closeness friendship_closeness_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_pkey PRIMARY KEY (id); -- @@ -4619,6 +4920,22 @@ ALTER TABLE ONLY vibetype.friendship ADD CONSTRAINT friendship_pkey PRIMARY KEY (id); +-- +-- Name: friendship_request friendship_request_account_id_friend_account_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_friend_account_id_key UNIQUE (account_id, friend_account_id); + + +-- +-- Name: friendship_request friendship_request_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_pkey PRIMARY KEY (id); + + -- -- Name: guest guest_event_id_contact_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -4949,31 +5266,31 @@ COMMENT ON INDEX vibetype.idx_event_upload_is_header_image_unique IS 'Ensures th -- --- Name: idx_friendship_created_by; Type: INDEX; Schema: vibetype; Owner: ci +-- Name: idx_friendship_closeness_created_by; Type: INDEX; Schema: vibetype; Owner: ci -- -CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); +CREATE INDEX idx_friendship_closeness_created_by ON vibetype.friendship_closeness USING btree (created_by); -- --- Name: INDEX idx_friendship_created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: idx_friendship_closeness_updated_by; Type: INDEX; Schema: vibetype; Owner: ci -- -COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; +CREATE INDEX idx_friendship_closeness_updated_by ON vibetype.friendship_closeness USING btree (updated_by); -- --- Name: idx_friendship_updated_by; Type: INDEX; Schema: vibetype; Owner: ci +-- Name: idx_friendship_created_by; Type: INDEX; Schema: vibetype; Owner: ci -- -CREATE INDEX idx_friendship_updated_by ON vibetype.friendship USING btree (updated_by); +CREATE INDEX idx_friendship_created_by ON vibetype.friendship USING btree (created_by); -- --- Name: INDEX idx_friendship_updated_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- Name: INDEX idx_friendship_created_by; Type: COMMENT; Schema: vibetype; Owner: ci -- -COMMENT ON INDEX vibetype.idx_friendship_updated_by IS 'B-Tree index to optimize lookups by updater.'; +COMMENT ON INDEX vibetype.idx_friendship_created_by IS 'B-Tree index to optimize lookups by creator.'; -- @@ -5061,10 +5378,10 @@ CREATE TRIGGER vibetype_trigger_event_search_vector BEFORE INSERT OR UPDATE OF n -- --- Name: friendship vibetype_trigger_friendship_update; Type: TRIGGER; Schema: vibetype; Owner: ci +-- Name: friendship_closeness vibetype_trigger_friendship_closeness_update; Type: TRIGGER; Schema: vibetype; Owner: ci -- -CREATE TRIGGER vibetype_trigger_friendship_update BEFORE UPDATE ON vibetype.friendship FOR EACH ROW EXECUTE FUNCTION vibetype.trigger_metadata_update(); +CREATE TRIGGER vibetype_trigger_friendship_closeness_update BEFORE UPDATE ON vibetype.friendship_closeness FOR EACH ROW EXECUTE FUNCTION vibetype.trigger_metadata_update(); -- @@ -5281,19 +5598,35 @@ ALTER TABLE ONLY vibetype.event_upload -- --- Name: friendship friendship_a_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship_closeness fk_friendship_closeness; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- -ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_a_account_id_fkey FOREIGN KEY (a_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT fk_friendship_closeness FOREIGN KEY (account_id, friend_account_id) REFERENCES vibetype.friendship(account_id, friend_account_id) ON DELETE CASCADE; -- --- Name: friendship friendship_b_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_b_account_id_fkey FOREIGN KEY (b_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + ADD CONSTRAINT friendship_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_closeness friendship_closeness_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_closeness friendship_closeness_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_closeness + ADD CONSTRAINT friendship_closeness_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES vibetype.account(id) ON DELETE SET NULL; -- @@ -5305,11 +5638,35 @@ ALTER TABLE ONLY vibetype.friendship -- --- Name: friendship friendship_updated_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- Name: friendship friendship_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- ALTER TABLE ONLY vibetype.friendship - ADD CONSTRAINT friendship_updated_by_fkey FOREIGN KEY (updated_by) REFERENCES vibetype.account(id) ON DELETE SET NULL; + ADD CONSTRAINT friendship_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_account_id_fkey FOREIGN KEY (account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: friendship_request friendship_request_friend_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.friendship_request + ADD CONSTRAINT friendship_request_friend_account_id_fkey FOREIGN KEY (friend_account_id) REFERENCES vibetype.account(id) ON DELETE CASCADE; -- @@ -5750,26 +6107,120 @@ CREATE POLICY event_upload_select ON vibetype.event_upload FOR SELECT USING ((ev ALTER TABLE vibetype.friendship ENABLE ROW LEVEL SECURITY; -- --- Name: friendship friendship_existing; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship_closeness; Type: ROW SECURITY; Schema: vibetype; Owner: ci +-- + +ALTER TABLE vibetype.friendship_closeness ENABLE ROW LEVEL SECURITY; + +-- +-- Name: friendship_closeness friendship_closeness_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_delete ON vibetype.friendship_closeness FOR DELETE USING ((account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_closeness friendship_closeness_insert; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_existing ON vibetype.friendship USING ((((vibetype.invoker_account_id() = a_account_id) AND (NOT (b_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))) OR ((vibetype.invoker_account_id() = b_account_id) AND (NOT (a_account_id IN ( SELECT account_block_ids.id - FROM vibetype_private.account_block_ids() account_block_ids(id))))))) WITH CHECK (false); +CREATE POLICY friendship_closeness_insert ON vibetype.friendship_closeness FOR INSERT WITH CHECK (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship_closeness friendship_closeness_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_not_blocked ON vibetype.friendship_closeness AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_closeness friendship_closeness_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_select ON vibetype.friendship_closeness FOR SELECT USING ((account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_closeness friendship_closeness_update; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_closeness_update ON vibetype.friendship_closeness FOR UPDATE USING ((account_id = vibetype.invoker_account_id())) WITH CHECK ((updated_by = vibetype.invoker_account_id())); + + +-- +-- Name: friendship friendship_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_delete ON vibetype.friendship FOR DELETE USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); -- -- Name: friendship friendship_insert; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); +CREATE POLICY friendship_insert ON vibetype.friendship FOR INSERT WITH CHECK ((((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.account_id, + friendship_request.friend_account_id, + friendship_request.account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))) OR ((account_id, friend_account_id, created_by) IN ( SELECT friendship_request.friend_account_id, + friendship_request.account_id, + friendship_request.friend_account_id + FROM vibetype.friendship_request + WHERE (friendship_request.friend_account_id = vibetype.invoker_account_id()))))); + + +-- +-- Name: friendship friendship_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_not_blocked ON vibetype.friendship AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_request; Type: ROW SECURITY; Schema: vibetype; Owner: ci +-- + +ALTER TABLE vibetype.friendship_request ENABLE ROW LEVEL SECURITY; + +-- +-- Name: friendship_request friendship_request_delete; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_delete ON vibetype.friendship_request FOR DELETE USING ((friend_account_id = vibetype.invoker_account_id())); + + +-- +-- Name: friendship_request friendship_request_insert; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_insert ON vibetype.friendship_request FOR INSERT WITH CHECK ((created_by = vibetype.invoker_account_id())); -- --- Name: friendship friendship_update; Type: POLICY; Schema: vibetype; Owner: ci +-- Name: friendship_request friendship_request_not_blocked; Type: POLICY; Schema: vibetype; Owner: ci -- -CREATE POLICY friendship_update ON vibetype.friendship FOR UPDATE USING ((status = 'requested'::vibetype.friendship_status)) WITH CHECK (((status = 'accepted'::vibetype.friendship_status) AND (updated_by = vibetype.invoker_account_id()))); +CREATE POLICY friendship_request_not_blocked ON vibetype.friendship_request AS RESTRICTIVE USING (((NOT (account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))) AND (NOT (friend_account_id IN ( SELECT account_block_ids.id + FROM vibetype_private.account_block_ids() account_block_ids(id)))))); + + +-- +-- Name: friendship_request friendship_request_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_request_select ON vibetype.friendship_request FOR SELECT USING (((account_id = vibetype.invoker_account_id()) OR (friend_account_id = vibetype.invoker_account_id()))); + + +-- +-- Name: friendship friendship_select; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY friendship_select ON vibetype.friendship FOR SELECT USING (true); -- @@ -6197,6 +6648,46 @@ GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_account; GRANT ALL ON FUNCTION vibetype.events_organized() TO vibetype_anonymous; +-- +-- Name: FUNCTION friendship_accept(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_accept(requestor_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_cancel(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_cancel(friend_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_reject(requestor_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_reject(requestor_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_request(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_request(friend_account_id uuid) TO vibetype_account; + + +-- +-- Name: FUNCTION friendship_toggle_closeness(friend_account_id uuid); Type: ACL; Schema: vibetype; Owner: ci +-- + +REVOKE ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) FROM PUBLIC; +GRANT ALL ON FUNCTION vibetype.friendship_toggle_closeness(friend_account_id uuid) TO vibetype_account; + + -- -- Name: FUNCTION guest_claim_array(); Type: ACL; Schema: vibetype; Owner: ci -- @@ -6551,7 +7042,21 @@ GRANT SELECT,INSERT,DELETE ON TABLE vibetype.event_upload TO vibetype_account; -- Name: TABLE friendship; Type: ACL; Schema: vibetype; Owner: ci -- -GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship TO vibetype_account; +GRANT SELECT,INSERT,DELETE ON TABLE vibetype.friendship TO vibetype_account; + + +-- +-- Name: TABLE friendship_closeness; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE vibetype.friendship_closeness TO vibetype_account; + + +-- +-- Name: TABLE friendship_request; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT,INSERT,DELETE ON TABLE vibetype.friendship_request TO vibetype_account; -- diff --git a/test/logic/scenario/database/index.sql b/test/logic/scenario/database/index.sql index f397cb56..8c333049 100644 --- a/test/logic/scenario/database/index.sql +++ b/test/logic/scenario/database/index.sql @@ -153,7 +153,11 @@ SELECT vibetype_test.index_existence( ); SELECT vibetype_test.index_existence( - ARRAY ['idx_friendship_created_by', 'idx_friendship_updated_by'] + ARRAY ['idx_friendship_created_by'] +); + +SELECT vibetype_test.index_existence( + ARRAY ['idx_friendship_closeness_created_by', 'idx_friendship_closeness_updated_by'] ); SELECT vibetype_test.index_existence( diff --git a/test/logic/scenario/model/friendship.sql b/test/logic/scenario/model/friendship.sql index b6b6d358..b3579501 100644 --- a/test/logic/scenario/model/friendship.sql +++ b/test/logic/scenario/model/friendship.sql @@ -1,4 +1,4 @@ -\echo test_friendship.. +\echo test_friendship... BEGIN; @@ -7,51 +7,150 @@ DECLARE accountA UUID; accountB UUID; accountC UUID; - friendshipAB UUID; - friendshipAC UUID; - friendshipCA UUID; + rec RECORD; + _is_close_friend BOOLEAN; + _invoker_account_id UUID; BEGIN - -- before all + -- create accounts accountA := vibetype_test.account_registration_verified('username-a', 'email+a@example.com'); accountB := vibetype_test.account_registration_verified('username-b', 'email+b@example.com'); accountC := vibetype_test.account_registration_verified('username-c', 'email+c@example.com'); + PERFORM vibetype_test.friendship_request_test('before A sends request to B', accountA, accountA, accountB, false); + -- friendship request from user A to B - friendshipAB := vibetype_test.friendship_request(accountA, accountB); - PERFORM vibetype_test.friendship_test('The friendship is requested for user A', accountA, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is requested for user B', accountB, 'requested', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A has no friends', accountA, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B has no friends', accountB, ARRAY[]::UUID[]); - - -- friendship acceptance - PERFORM vibetype_test.friendship_accept(accountB, friendshipAB); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('The friendship is accepted for user B', accountB, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user A', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_test('There is no requested friendship for user B', accountA, 'requested', ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User A is a friend of user B', accountB, ARRAY[accountA]::UUID[]); + PERFORM vibetype_test.friendship_request(accountA, accountB); + + PERFORM vibetype_test.friendship_request_test('after A sends request to B (1)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_request_test('after A sends request to B (2)', accountB, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_test('after A sends request to B (4)', accountB, accountB, accountA, false); + PERFORM vibetype_test.friendship_request_test('C cannot seen the friendship request from A to B', accountC, accountA, accountB, false); + + -- B accepts A's friendship request + PERFORM vibetype_test.friendship_accept(accountB, accountA); + + PERFORM vibetype_test.friendship_request_test('B accepts friendship request from A (1)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (2)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('B accepts friendship request from A (3)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_test('C can also see the friendship between A and B (1)', accountC, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('C can also see the friendship between A and B (2)', accountC, accountB, accountA, true); -- friendship request from user C to A - friendshipCA := vibetype_test.friendship_request(accountC, accountA); - PERFORM vibetype_test.friendship_test('There is still only one accepted friendship for user A', accountA, 'accepted', ARRAY[friendshipAB]::UUID[]); - PERFORM vibetype_test.friendship_test('There is a new requested friendship for user C', accountC, 'requested', ARRAY[friendshipCA]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('User C has no friends', accountC, ARRAY[]::UUID[]); + PERFORM vibetype_test.friendship_request(accountC, accountA); + + PERFORM vibetype_test.friendship_request_test('after C sends request to A (1)', accountC, accountC, accountA, true); + PERFORM vibetype_test.friendship_test('after C sends request to A (2)', accountC, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('after A sends request to B (3)', accountA, accountA, accountC, false); + PERFORM vibetype_test.friendship_test('User B is still a friend of user A (1)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_test('User A is still a friend of user B (2)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_test('User C is not a friend of A', accountC, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('User C is not a friend of B', accountC, accountC, accountA, false); + BEGIN + -- C sends another request to A, should lead to exception VTREQ + PERFORM vibetype_test.friendship_request(accountA, accountC); + RAISE 'C sends another request to A: it was possible to request a friendship more than once.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; + END; BEGIN - friendshipAC := vibetype_test.friendship_request(accountA, accountC); - RAISE 'It was possible to requested a friendship more than once.'; + -- A sends a request to C, should lead to exception VTREQ + PERFORM vibetype_test.friendship_request(accountA, accountC); + RAISE 'A sends a request to C: it was possible to request a friendship more than once.'; EXCEPTION - WHEN unique_violation THEN -- do nothing as expected - WHEN OTHERS THEN RAISE; + WHEN OTHERS THEN + IF SQLSTATE != 'VTREQ' THEN + RAISE; + END IF; END; - -- friendship rejection - PERFORM vibetype_test.friendship_reject(accountA, friendshipCA); - PERFORM vibetype_test.friendship_test('After user A rejected user C''s friendship request, the friendship is removed for user C', accountC, NULL, ARRAY[]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user B is still a friend of user A', accountA, ARRAY[accountB]::UUID[]); - PERFORM vibetype_test.friendship_account_ids_test('After user A rejected user C''s friendship request, user C has no friends anymore', accountC, ARRAY[]::UUID[]); + BEGIN + -- A sends a new request to B, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountA, accountB); + RAISE 'A sends a new request to B: it was possible to request for an already existing friendship.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFEX' THEN + RAISE; + END IF; + END; + + BEGIN + -- B sends a new request to A, should lead to exception VTFEX + PERFORM vibetype_test.friendship_request(accountB, accountA); + RAISE 'It was possible to request for an already existing friendship.'; + EXCEPTION + WHEN OTHERS THEN + IF SQLSTATE != 'VTFEX' THEN + RAISE; + END IF; + END; + + -- A rejects friendship request from C + PERFORM vibetype_test.friendship_reject(accountA, accountC); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (1)', accountA, accountC, accountA, false); + PERFORM vibetype_test.friendship_test('After A rejected C''s friendship request (2)', accountC, accountC, accountA, false); + + -- a new friendship request from user C to A, this time accepted by A + PERFORM vibetype_test.friendship_request(accountC, accountA); + PERFORM vibetype_test.friendship_accept(accountA, accountC); + PERFORM vibetype_test.friendship_test('C is a friend of A (1)', accountA, accountA, accountC, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (2)', accountA, accountC, accountA, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (3)', accountC, accountA, accountC, true); + PERFORM vibetype_test.friendship_test('C is a friend of A (4)', accountC, accountC, accountA, true); + + -- friendship request from user B to C + PERFORM vibetype_test.friendship_request(accountB, accountC); + + -- B marks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + +/* + RAISE NOTICE '----'; + FOR rec IN + SELECT a.username, b.username as friend_username, f.is_close_friend + FROM vibetype.friendship f + JOIN vibetype.account a ON f.account_id = a.id + JOIN vibetype.account b ON f.friend_account_id = b.id + LOOP + RAISE NOTICE 'friendship: account = %, friend_account = %, is_close_friend = %', rec.username, rec.friend_username, rec.is_close_friend; + END LOOP; +*/ + + -- Test: closeness is also visible for the account that declared or cancelled this property + -- towards one of the account's friends + + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (1)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (4)', accountA, accountA, accountB, false); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); + + -- A marks B as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountA, accountB); + + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (1)', accountB, accountB, accountA, true); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('A marks B as a close friend (4)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); + + -- B unmarks A as a close friend + PERFORM vibetype_test.friendship_toggle_closeness(accountB, accountA); + + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (1)', accountB, accountB, accountA, false); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (2)', accountB, accountA, accountB, null); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (3)', accountA, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B unmarks A as a close friend (4)', accountA, accountA, accountB, true); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (5)', accountC, accountB, accountA, null); + PERFORM vibetype_test.friendship_closeness_test('B marks A as a close friend (6)', accountC, accountA, accountB, null); + END $$; ROLLBACK; diff --git a/test/logic/utility/model/account_registration.sql b/test/logic/utility/model/account_registration.sql index 19d943f9..5741a410 100644 --- a/test/logic/utility/model/account_registration.sql +++ b/test/logic/utility/model/account_registration.sql @@ -8,7 +8,7 @@ DECLARE _verification UUID; BEGIN _legal_term_id := vibetype_test.legal_term_select_by_singleton(); - PERFORM vibetype.account_registration('1970-01-01', _email_address, 'en', _legal_term_id, 'password', _username); + PERFORM vibetype.account_registration(to_date('1970-01-01', 'yyyy-mm-dd'), _email_address, 'en', _legal_term_id, 'password', _username); SELECT id INTO _account_id FROM vibetype.account diff --git a/test/logic/utility/model/friendship.sql b/test/logic/utility/model/friendship.sql index 78337634..69e9626b 100644 --- a/test/logic/utility/model/friendship.sql +++ b/test/logic/utility/model/friendship.sql @@ -1,6 +1,6 @@ CREATE OR REPLACE FUNCTION vibetype_test.friendship_accept ( _invoker_account_id UUID, - _id UUID + _requestor_account_id UUID ) RETURNS VOID AS $$ DECLARE rec RECORD; @@ -9,9 +9,7 @@ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - UPDATE vibetype.friendship - SET "status" = 'accepted'::vibetype.friendship_status - WHERE id = _id; + PERFORM vibetype.friendship_accept(_requestor_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; @@ -19,16 +17,31 @@ END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_accept(UUID, UUID) TO vibetype_account; +CREATE OR REPLACE FUNCTION vibetype_test.friendship_cancel ( + _invoker_account_id UUID, + _friend_account_id UUID +) RETURNS VOID AS $$ +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + PERFORM vibetype.friendship_cancel(_friend_account_id); + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_cancel(UUID, UUID) TO vibetype_account; + + CREATE OR REPLACE FUNCTION vibetype_test.friendship_reject ( _invoker_account_id UUID, - _id UUID + _friend_account_id UUID ) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - DELETE FROM vibetype.friendship - WHERE id = _id; + PERFORM vibetype.friendship_reject(_friend_account_id); SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; @@ -39,129 +52,121 @@ GRANT EXECUTE ON FUNCTION vibetype_test.friendship_reject(UUID, UUID) TO vibetyp CREATE OR REPLACE FUNCTION vibetype_test.friendship_request ( _invoker_account_id UUID, _friend_account_id UUID -) RETURNS UUID AS $$ -DECLARE - _id UUID; - _a_account_id UUID; - _b_account_id UUID; +) RETURNS VOID AS $$ BEGIN SET LOCAL role = 'vibetype_account'; EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - IF _invoker_account_id < _friend_account_id THEN - _a_account_id := _invoker_account_id; - _b_account_id := _friend_account_id; - ELSE - _a_account_id := _friend_account_id; - _b_account_id := _invoker_account_id; - END IF; - - INSERT INTO vibetype.friendship(a_account_id, b_account_id, created_by) - VALUES (_a_account_id, _b_account_id, _invoker_account_id) - RETURNING id INTO _id; + PERFORM vibetype.friendship_request(_friend_account_id); SET LOCAL ROLE NONE; - - RETURN _id; END $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request(UUID, UUID) TO vibetype_account; +CREATE OR REPLACE FUNCTION vibetype_test.friendship_toggle_closeness ( + _invoker_account_id UUID, + _friend_account_id UUID +) RETURNS VOID AS $$ +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + PERFORM vibetype.friendship_toggle_closeness(_friend_account_id); + + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_toggle_closeness(UUID, UUID) TO vibetype_account; + + CREATE OR REPLACE FUNCTION vibetype_test.friendship_test ( _test_case TEXT, _invoker_account_id UUID, - _status TEXT, -- status IS NULL means "any status" - _expected_result UUID[] + _account_id UUID, + _friend_account_id UUID, -- _friend_account_id IS NULL means "any friend" + _expected_result BOOLEAN ) RETURNS VOID AS $$ DECLARE - rec RECORD; + _result BOOLEAN; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL role = 'vibetype_anonymous'; - SET LOCAL jwt.claims.account_id = ''; - ELSE - SET LOCAL role = 'vibetype_account'; - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT TRUE INTO _result + FROM vibetype.friendship + WHERE account_id = _account_id + AND friend_account_id = _friend_account_id; + + IF _result IS NULL THEN + _result := FALSE; END IF; - IF EXISTS ( - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the query result'; + IF _result != _expected_result THEN + RAISE EXCEPTION '%: expected result was % but result is %.', _test_case, _expected_result, _result USING ERRCODE = 'VTTST'; END IF; - IF EXISTS ( - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT id FROM vibetype.friendship WHERE _status IS NULL OR status = _status::vibetype.friendship_status - ) THEN - RAISE EXCEPTION 'some account is missing in the query result'; + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; + +CREATE OR REPLACE FUNCTION vibetype_test.friendship_closeness_test ( + _test_case TEXT, + _invoker_account_id UUID, + _account_id UUID, + _friend_account_id UUID, + _expected_result BOOLEAN +) RETURNS VOID AS $$ +DECLARE + _result BOOLEAN; +BEGIN + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT is_close_friend INTO _result + FROM vibetype.friendship_closeness + WHERE account_id = _account_id + AND friend_account_id = _friend_account_id; + + IF _result != _expected_result THEN + RAISE EXCEPTION '%: expected result was % but result is %.', _test_case, _expected_result, _result USING ERRCODE = 'VTFCT'; END IF; SET LOCAL ROLE NONE; END $$ LANGUAGE plpgsql; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_test(TEXT, UUID, TEXT, UUID[]) TO vibetype_account; +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_closeness_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account; -CREATE OR REPLACE FUNCTION vibetype_test.friendship_account_ids_test ( +CREATE OR REPLACE FUNCTION vibetype_test.friendship_request_test ( _test_case TEXT, _invoker_account_id UUID, - _expected_result UUID[] + _account_id UUID, + _friend_account_id UUID, + _expected_to_exist BOOLEAN ) RETURNS VOID AS $$ DECLARE - rec RECORD; + _id UUID; BEGIN - IF _invoker_account_id IS NULL THEN - SET LOCAL jwt.claims.account_id = ''; - ELSE - EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; - END IF; + SET LOCAL role = 'vibetype_account'; + EXECUTE 'SET LOCAL jwt.claims.account_id = ''' || _invoker_account_id || ''''; + + SELECT id INTO _id + FROM vibetype.friendship_request + WHERE account_id = _account_id + AND friend_account_id = _friend_account_id; - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = _invoker_account_id - and status = 'accepted'::vibetype.friendship_status - ) - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - EXCEPT - SELECT * FROM unnest(_expected_result) - ) THEN - RAISE EXCEPTION 'some accounts should not appear in the list of friends'; + IF _id IS NULL AND _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request expected to exist but not present.', _test_case USING ERRCODE = 'VTFRT'; END IF; - IF EXISTS ( - WITH friendship_account_ids_test AS ( - SELECT b_account_id as account_id - FROM vibetype.friendship - WHERE a_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - UNION ALL - SELECT a_account_id as account_id - FROM vibetype.friendship - WHERE b_account_id = vibetype.invoker_account_id() - and status = 'accepted'::vibetype.friendship_status - ) - SELECT * FROM unnest(_expected_result) - EXCEPT - SELECT account_id as id - FROM friendship_account_ids_test - WHERE account_id NOT IN (SELECT b.id FROM vibetype_private.account_block_ids() b) - ) THEN - RAISE EXCEPTION 'some account is missing in the list of friends'; + IF _id IS NOT NULL AND NOT _expected_to_exist THEN + RAISE EXCEPTION '%: friendship request exists but is not expected to exist.', _test_case USING ERRCODE = 'VTFRT'; END IF; -END $$ LANGUAGE plpgsql SECURITY DEFINER; -GRANT EXECUTE ON FUNCTION vibetype_test.friendship_account_ids_test(TEXT, UUID, UUID[]) TO vibetype_account; + SET LOCAL ROLE NONE; +END $$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION vibetype_test.friendship_request_test(TEXT, UUID, UUID, UUID, BOOLEAN) TO vibetype_account;