diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | TODOs.adoc | 6 | ||||
-rwxr-xr-x | mkdeps.sh | 39 | ||||
-rw-r--r-- | src/papod.go | 3066 | ||||
-rw-r--r-- | tests/papod.go | 4530 | ||||
-rw-r--r-- | tests/queries.sql | 1081 |
6 files changed, 7328 insertions, 1396 deletions
@@ -61,7 +61,7 @@ derived-assets = \ $(NAME).bin \ side-assets = \ - $(NAME).db* \ + *$(NAME).db* \ tests/functional/*/*.go.db* \ tests/fuzz/corpus/ \ tests/benchmarks/*/main.txt \ @@ -23,9 +23,9 @@ -So we don't recompile {over-200k-lines} of code on every build of the application. -Not only that, but also for properly separating projects into their own -artifacts. +So we don't recompile {over-200k-lines} of code on every build of the +application. Not only that, but also for properly separating projects into +their own artifacts. [[td-x9yOtxlTfUQ]] @@ -4,26 +4,37 @@ set -eu export LANG=POSIX.UTF-8 +no_main() { + grep -v '/main\.go$' +} + +only_main() { + grep '/main\.go$' +} + +sources() { + find src tests -name '*.go' | grep -v '^src/version\.go$' +} + libs() { - find src tests -name '*.go' | grep -v '/main\.go$' | - grep -v '/version\.go$' + sources | no_main } mains() { - find src tests -name '*.go' | grep '/main\.go$' + sources | only_main } libs | varlist 'libs.go' mains | varlist 'mains.go' -find tests/functional/*/*.go -not -name main.go | varlist 'functional-tests/lib.go' -find tests/functional/*/main.go | varlist 'functional-tests/main.go' -find tests/fuzz/*/*.go -not -name main.go | varlist 'fuzz-targets/lib.go' -find tests/fuzz/*/main.go | varlist 'fuzz-targets/main.go' -find tests/benchmarks/*/*.go -not -name main.go | varlist 'benchmarks/lib.go' -find tests/benchmarks/*/main.go | varlist 'benchmarks/main.go' - -{ libs; mains; } | sort | sed 's/^\(.*\)\.go$/\1.a:\t\1.go/' -mains | sort | sed 's/^\(.*\)\.go$/\1.bin:\t\1.a/' -mains | sort | sed 's/^\(.*\)\.go$/\1.bin-check:\t\1.bin/' -mains | sort | sed 's|^\(.*\)/main\.go$|\1/main.a:\t\1/$(NAME).a|' +find tests/functional/*/*.go | no_main | varlist 'functional-tests/lib.go' +find tests/functional/*/main.go | varlist 'functional-tests/main.go' +find tests/fuzz/*/*.go | no_main | varlist 'fuzz-targets/lib.go' +find tests/fuzz/*/main.go | varlist 'fuzz-targets/main.go' +find tests/benchmarks/*/*.go | no_main | varlist 'benchmarks/lib.go' +find tests/benchmarks/*/main.go | varlist 'benchmarks/main.go' + +sources | sort | sed 's/^\(.*\)\.go$/\1.a:\t\1.go/' +mains | sort | sed 's/^\(.*\)\.go$/\1.bin:\t\1.a/' +mains | sort | sed 's/^\(.*\)\.go$/\1.bin-check:\t\1.bin/' +mains | sort | sed 's|^\(.*\)/main\.go$|\1/main.a:\t\1/$(NAME).a|' diff --git a/src/papod.go b/src/papod.go index 4bc5b4d..710c87c 100644 --- a/src/papod.go +++ b/src/papod.go @@ -52,25 +52,29 @@ type queriesT struct{ userByUUID func(guuid.UUID) (userT, error) updateUser func(userT) error deleteUser func(guuid.UUID) error - addNetwork func(userT, newNetworkT) (networkT, error) + addNetwork func(userT, newNetworkT, guuid.UUID) (networkT, error) getNetwork func(userT, guuid.UUID) (networkT, error) networks func(userT, func(networkT) error) error - setNetwork func(userT, networkT) error - nipNetwork func(userT, guuid.UUID) error - addMember func(userT, networkT, newMemberT) (memberT, error) - showMember func(userT, guuid.UUID) (memberT, error) - members func(userT, guuid.UUID, func(memberT) error) error - editMember func(userT, memberT) error - dropMember func(userT, guuid.UUID) error - addChannel func(guuid.UUID, newChannelT) (channelT, error) - channels func(guuid.UUID, func(channelT) error) error - topic func(channelT) error - endChannel func(guuid.UUID) error - join func(guuid.UUID, guuid.UUID) error - part func(guuid.UUID, guuid.UUID) error - names func(guuid.UUID, func(memberT) error) error + setNetwork func(memberT, networkT) error + nipNetwork func(memberT) error + membership func(userT, networkT) (memberT, error) + addMember func(memberT, newMemberT) (memberT, error) + addRole func(memberT, string, memberT) error + dropRole func(memberT, string, memberT) error + showMember func(memberT, guuid.UUID) (memberT, error) + members func(memberT, func(memberT) error) error + editMember func(memberT, memberT) error + dropMember func(memberT, guuid.UUID) error + addChannel func(memberT, newChannelT) (channelT, error) + chanByName func(memberT, string) (channelT, error) + channels func(memberT, func(channelT) error) error + setChannel func(memberT, channelT) error + endChannel func(memberT, guuid.UUID) error + join func(memberT, guuid.UUID) error + part func(memberT, channelT) error + names func(memberT, guuid.UUID, func(memberT) error) error addEvent func(newEventT) (eventT, error) - allAfter func(guuid.UUID, func(eventT) error) error + allAfter func(memberT, guuid.UUID, func(eventT) error) error logMessage func(userT, messageT) error close func() error } @@ -97,6 +101,13 @@ const ( NetworkType_Unlisted NetworkType = "unlisted" ) +type MemberStatus string +const ( + MemberStatus_Active MemberStatus = "active" + MemberStatus_Inactive MemberStatus = "inactive" + MemberStatus_Removed MemberStatus = "removed" +) + type newNetworkT struct{ uuid guuid.UUID name string @@ -108,28 +119,33 @@ type networkT struct{ id int64 timestamp time.Time uuid guuid.UUID - createdBy guuid.UUID name string description string type_ NetworkType } type newMemberT struct{ - userID guuid.UUID + userID guuid.UUID + memberID guuid.UUID + username string } type memberT struct{ - id int64 - timestamp time.Time - uuid guuid.UUID + id int64 + timestamp time.Time + uuid guuid.UUID + username string + displayName string + pictureID *guuid.UUID + status MemberStatus + roles []string } type newChannelT struct{ id int64 timestamp time.Time uuid guuid.UUID - // networkID guuid.UUID FIXME - publicName string + publicName *string label string description string virtual bool @@ -139,48 +155,65 @@ type channelT struct{ id int64 timestamp time.Time uuid guuid.UUID - // networkID guuid.UUID FIXME - publicName string + publicName *string label string description string virtual bool } +type SourceType string +const ( + SourceType_Logon SourceType = "logon" +) + +type sourceT struct{ + uuid guuid.UUID + type_ SourceType + metadata *map[string]interface{} +} + +type EventType string +const ( + EventType_UserJoin EventType = "user-join" + EventType_UserMessage EventType = "user-message" +) + type newEventT struct{ - eventID guuid.UUID - channelID guuid.UUID - connectionID guuid.UUID - type_ string - payload string + eventID guuid.UUID + channelID guuid.UUID + source sourceT + type_ EventType + payload string + metadata *map[string]interface{} } type eventT struct{ - id int64 - timestamp time.Time - uuid guuid.UUID - channelID guuid.UUID - connectionID guuid.UUID - type_ string - payload string - previous *eventT - isFist bool + id int64 + timestamp time.Time + uuid guuid.UUID + channelID guuid.UUID + source sourceT + type_ EventType + payload string + metadata *map[string]interface{} } -type messageParamsT struct{ - middle []string - trailing string +type eventEntryT struct{ + event eventT + previous *eventT + isFirst bool } type messageT struct{ prefix string command string - params messageParamsT + params []string raw string } type replyT struct{ command string - params messageParamsT + params []string } type listenersT struct{ @@ -196,22 +229,16 @@ type consumerT struct{ handlerFn func(papodT) func(fiinha.Message) error } +type netConnI interface{ + Write(p []byte) (n int, err error) + Close() error +} + type connectionT struct{ - conn net.Conn uuid guuid.UUID user *userT -} - -type receiverT struct{ - send func(messageT) - close func() -} - -type receiversT struct{ - add func(receiverT) - remove func(receiverT) - get func(guuid.UUID) []receiverT - close func() + conn netConnI + send func(messageT) } type metricsT struct{ @@ -228,7 +255,7 @@ type papodT struct{ queries queriesT listeners listenersT consumers []consumerT - receivers receiversT + state stateT metrics metricsT // logger g.Logger } @@ -329,134 +356,271 @@ func inTx(db *sql.DB, fn func(*sql.Tx) error) error { /// treated only as opaque IDs. func createTablesSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME: unconfirmed premise: statements within a trigger are - -- part of the transaction that caused it, and so are - -- atomic. + -- TODO: unconfirmed premise: statements within a trigger are + -- part of the transaction that caused it, and so are + -- atomic. -- See also: -- https://stackoverflow.com/questions/77441888/ -- https://stackoverflow.com/questions/30511116/ CREATE TABLE IF NOT EXISTS "%s_users" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), -- provided by cracha - uuid BLOB NOT NULL UNIQUE, + user_uuid BLOB NOT NULL UNIQUE, username TEXT NOT NULL, display_name TEXT NOT NULL, picture_uuid BLOB UNIQUE, deleted INT NOT NULL CHECK(deleted IN (0, 1)) ) STRICT; --- CREATE TABLE IF NOT EXISTS "%s_user_changes" ( --- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, --- timestamp TEXT NOT NULL DEFAULT (%s), --- user_id INTEGER NOT NULL REFERENCES "%s_users"(id), --- attribute TEXT NOT NULL CHECK( --- attribute IN ( --- 'username', --- 'display_name', --- 'picture_uuid', --- 'deleted' --- ) --- ), --- value TEXT NOT NULL, --- op INT NOT NULL CHECK(op IN (0, 1)) --- ) STRICT; --- CREATE TRIGGER IF NOT EXISTS "%s_user_creation" --- AFTER INSERT ON "%s_users" --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'username', NEW.username, true), --- (NEW.id, 'display_name', NEW.display_name, true), --- (NEW.id, 'deleted', NEW.deleted, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "%s_user_creation_picture_uuid" --- AFTER INSERT ON "%s_users" --- WHEN NEW.picture_uuid != NULL --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'picture_uuid', NEW.picture_uuid, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "%s_user_update_username" --- AFTER UPDATE ON "%s_users" --- WHEN OLD.username != NEW.username --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'username', OLD.username, false), --- (NEW.id, 'username', NEW.username, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "%s_user_update_display_name" --- AFTER UPDATE ON "%s_users" --- WHEN OLD.display_name != NEW.display_name --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'display_name', OLD.display_name, false), --- (NEW.id, 'display_name', NEW.display_name, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "%s_user_update_picture_uuid" --- AFTER UPDATE ON "%s_users" --- WHEN OLD.picture_uuid != NEW.picture_uuid --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'picture_uuid', OLD.picture_uuid, false), --- (NEW.id, 'picture_uuid', NEW.picture_uuid, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "%s_user_update_deleted" --- AFTER UPDATE ON "%s_users" --- WHEN OLD.deleted != NEW.deleted --- BEGIN --- INSERT INTO "%s_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'deleted', OLD.deleted, false), --- (NEW.id, 'deleted', NEW.deleted, true) --- ; --- END; + CREATE TABLE IF NOT EXISTS "%s_user_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + user_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'username', + 'display_name', + 'picture_uuid', + 'deleted' + ) + ), + value_text TEXT, + value_blob BLOB, + value_bool INT CHECK(value_bool IN (0, 1)), + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; + CREATE TRIGGER IF NOT EXISTS "%s_user_new" + AFTER INSERT ON "%s_users" + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', NEW.username, true), + (NEW.id, 'display_name', NEW.display_name, true) + ; + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_new_picture_uuid" + AFTER INSERT ON "%s_users" + WHEN NEW.picture_uuid IS NOT NULL + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_update_username" + AFTER UPDATE ON "%s_users" + WHEN OLD.username != NEW.username + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', OLD.username, false), + (NEW.id, 'username', NEW.username, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_update_display_name" + AFTER UPDATE ON "%s_users" + WHEN OLD.display_name != NEW.display_name + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'display_name', OLD.display_name, false), + (NEW.id, 'display_name', NEW.display_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_add_picture_uuid" + AFTER UPDATE ON "%s_users" + WHEN ( + OLD.picture_uuid IS NULL AND + NEW.picture_uuid IS NOT NULL + ) + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_remove_picture_uuid" + AFTER UPDATE ON "%s_users" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NULL + ) + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_update_picture_uuid" + AFTER UPDATE ON "%s_users" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NOT NULL AND + OLD.picture_uuid != NEW.picture_uuid + ) + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false), + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_user_update_deleted" + AFTER UPDATE ON "%s_users" + WHEN OLD.deleted != NEW.deleted + BEGIN + INSERT INTO "%s_user_changes" ( + user_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'deleted', OLD.deleted, false), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + + CREATE TABLE IF NOT EXISTS "%s_sessions" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + -- provided by cracha + session_uuid BLOB NOT NULL UNIQUE, + user_id INTEGER NOT NULL + REFERENCES "%s_users"(id), + finished_at TEXT + ); + CREATE TABLE IF NOT EXISTS "%s_connections" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + uuid BLOB NOT NULL UNIQUE, + finished_at TEXT + ); + CREATE TABLE IF NOT EXISTS "%s_logons" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + session_id INTEGER NOT NULL + REFERENCES "%s_sessions"(id), + connection_id INTEGER NOT NULL + REFERENCES "%s_connections"(id), + UNIQUE (session_id, connection_id) + ) STRICT; CREATE TABLE IF NOT EXISTS "%s_networks" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), uuid BLOB NOT NULL UNIQUE, - creator_id INTEGER NOT NULL REFERENCES "%s_users"(id), name TEXT NOT NULL, description TEXT NOT NULL, type TEXT NOT NULL CHECK( type IN ('public', 'private', 'unlisted') - ) + ), + deleted INT NOT NULL CHECK(deleted IN (0, 1)) ) STRICT; + CREATE INDEX IF NOT EXISTS "%s_networks_type" + ON "%s_networks"(type); CREATE TABLE IF NOT EXISTS "%s_network_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), - network_id INTEGER NOT NULL - REFERENCES "%s_networks"(id), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + network_id INTEGER NOT NULL, attribute TEXT NOT NULL CHECK( attribute IN ( 'name', 'description', - 'type' + 'type', + 'deleted', + 'logon_id' -- FIXME ) ), value TEXT NOT NULL, op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "%s_network_new" + AFTER INSERT ON "%s_networks" + BEGIN + INSERT INTO "%s_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'name', NEW.name, true), + (NEW.id, 'description', NEW.description, true), + (NEW.id, 'type', NEW.type, true), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_network_update_name" + AFTER UPDATE ON "%s_networks" + WHEN OLD.name != NEW.name + BEGIN + INSERT INTO "%s_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'name', OLD.name, false), + (NEW.id, 'name', NEW.name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_network_update_description" + AFTER UPDATE ON "%s_networks" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "%s_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'description', OLD.description, false), + (NEW.id, 'description', NEW.description, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_network_update_type" + AFTER UPDATE ON "%s_networks" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "%s_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'type', OLD.type, false), + (NEW.id, 'type', NEW.type, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_network_update_deleted" + AFTER UPDATE ON "%s_networks" + WHEN OLD.deleted != NEW.deleted + BEGIN + INSERT INTO "%s_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'deleted', OLD.deleted, false), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; CREATE TABLE IF NOT EXISTS "%s_members" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + uuid BLOB NOT NULL UNIQUE, network_id INTEGER NOT NULL REFERENCES "%s_networks"(id), user_id INTEGER NOT NULL, @@ -472,6 +636,120 @@ func createTablesSQL(prefix string) queryT { UNIQUE (network_id, username, active_uniq), UNIQUE (network_id, user_id) ) STRICT; + CREATE TABLE IF NOT EXISTS "%s_member_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + member_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'username', + 'display_name', + 'picture_uuid', + 'status', + 'logon_id' -- FIXME + ) + ), + value_text TEXT, + value_blob BLOB, + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; + CREATE TRIGGER IF NOT EXISTS "%s_member_new" + AFTER INSERT ON "%s_members" + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', NEW.username, true), + (NEW.id, 'display_name', NEW.display_name, true), + (NEW.id, 'status', NEW.status, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_new_picture_uuid" + AFTER INSERT ON "%s_members" + WHEN NEW.picture_uuid IS NOT NULL + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_update_username" + AFTER UPDATE ON "%s_members" + WHEN OLD.username != NEW.username + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', OLD.username, false), + (NEW.id, 'username', NEW.username, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_update_display_name" + AFTER UPDATE ON "%s_members" + WHEN OLD.display_name != NEW.display_name + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'display_name', OLD.display_name, false), + (NEW.id, 'display_name', NEW.display_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_update_status" + AFTER UPDATE ON "%s_members" + WHEN OLD.status != NEW.status + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'status', OLD.status, false), + (NEW.id, 'status', NEW.status, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_add_picture_uuid" + AFTER UPDATE ON "%s_members" + WHEN ( + OLD.picture_uuid IS NULL AND + NEW.picture_uuid IS NOT NULL + ) + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_remove_picture_uuid" + AFTER UPDATE ON "%s_members" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NULL + ) + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_update_picture_uuid" + AFTER UPDATE ON "%s_members" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NOT NULL AND + OLD.picture_uuid != NEW.picture_uuid + ) + BEGIN + INSERT INTO "%s_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false), + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; CREATE TABLE IF NOT EXISTS "%s_member_roles" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -480,40 +758,157 @@ func createTablesSQL(prefix string) queryT { role TEXT NOT NULL, UNIQUE (member_id, role) ) STRICT; - - -- FIXME: use a trigger - CREATE TABLE IF NOT EXISTS "%s_member_changes" ( + CREATE TABLE IF NOT EXISTS "%s_member_role_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), - member_id INTEGER NOT NULL - REFERENCES "%s_members"(id), - attribute TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + role_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'role', + 'logon_id' -- FIXME + ) + ), value TEXT NOT NULL, op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "%s_member_role_add" + AFTER INSERT ON "%s_member_roles" + BEGIN + INSERT INTO "%s_member_role_changes" ( + role_id, attribute, value, op + ) VALUES + (NEW.id, 'role', NEW.role, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_member_role_remove" + AFTER DELETE ON "%s_member_roles" + BEGIN + INSERT INTO "%s_member_role_changes" ( + role_id, attribute, value, op + ) VALUES + (OLD.id, 'role', OLD.role, false) + ; + END; CREATE TABLE IF NOT EXISTS "%s_channels" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), uuid BLOB NOT NULL UNIQUE, - network_id INTEGER -- FIXME NOT NULL + network_id INTEGER NOT NULL REFERENCES "%s_networks"(id), - public_name TEXT UNIQUE, + public_name TEXT, label TEXT NOT NULL, description TEXT NOT NULL, - virtual INT NOT NULL CHECK(virtual IN (0, 1)) + virtual INT NOT NULL CHECK(virtual IN (0, 1)), + UNIQUE (network_id, public_name) ) STRICT; - - -- FIXME: use a trigger CREATE TABLE IF NOT EXISTS "%s_channel_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), - channel_id INTEGER NOT NULL - REFERENCES "%s_channels"(id), - attribute TEXT NOT NULL, - value TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + channel_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'public_name', + 'label', + 'description', + 'virtual', + 'logon_id' -- FIXME + ) + ), + value_text TEXT, + value_bool INT CHECK(value_bool IN (0, 1)), op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "%s_channel_new" + AFTER INSERT ON "%s_channels" + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'label', NEW.label, true), + (NEW.id, 'description', NEW.description, true) + ; + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'virtual', NEW.virtual, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_new_public_name" + AFTER INSERT ON "%s_channels" + WHEN NEW.public_name IS NOT NULL + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'public_name', NEW.public_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_update_label" + AFTER UPDATE ON "%s_channels" + WHEN OLD.label != NEW.label + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'label', OLD.label, false), + (NEW.id, 'label', NEW.label, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_update_description" + AFTER UPDATE ON "%s_channels" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'description', OLD.description, false), + (NEW.id, 'description', NEW.description, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_update_virtual" + AFTER UPDATE ON "%s_channels" + WHEN OLD.virtual != NEW.virtual + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'virtual', OLD.virtual, false), + (NEW.id, 'virtual', NEW.virtual, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_add_public_name" + AFTER UPDATE ON "%s_channels" + WHEN ( + OLD.public_name IS NULL AND + NEW.public_name IS NOT NULL + ) + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'public_name', NEW.public_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "%s_channel_remove_public_name" + AFTER UPDATE ON "%s_channels" + WHEN ( + OLD.public_name IS NOT NULL AND + NEW.public_name IS NULL + ) + BEGIN + INSERT INTO "%s_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (OLD.id, 'public_name', OLD.public_name, false) + ; + END; CREATE TABLE IF NOT EXISTS "%s_participants" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -523,29 +918,46 @@ func createTablesSQL(prefix string) queryT { REFERENCES "%s_members"(id), UNIQUE (channel_id, member_id) ) STRICT; + CREATE TABLE IF NOT EXISTS "%s_participant_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + %s + ), + participant_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'connection_id' + ) + ), + value TEXT NOT NULL, + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; - -- FIXME: create database table for connections? - -- A user can have multiple sessions (different browsers, - -- mobile, etc.), and each session has multiple connections, as - -- the user connects and disconnections using the same session - -- id, all while it is valid. - -- FIXME: can a connection have multiple sessions? A long-lived - -- connection that spans multiple sessions would fit into this. CREATE TABLE IF NOT EXISTS "%s_channel_events" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (%s), + timestamp TEXT NOT NULL DEFAULT ( + %s + ), uuid BLOB NOT NULL UNIQUE, channel_id INTEGER NOT NULL REFERENCES "%s_channels"(id), - connection_uuid BLOB NOT NULL, -- FIXME: join + source_uuid BLOB NOT NULL, + source_type TEXT NOT NULL CHECK( + source_type IN ( + 'logon' + ) + ), + source_metadata TEXT, type TEXT NOT NULL CHECK( type IN ( 'user-join', 'user-message' ) ), - payload TEXT NOT NULL + payload TEXT NOT NULL, + metadata TEXT ) STRICT; + ` return queryT{ write: fmt.Sprintf( @@ -574,29 +986,113 @@ func createTablesSQL(prefix string) queryT { prefix, prefix, prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + g.SQLiteNow, + prefix, + prefix, + g.SQLiteNow, + prefix, g.SQLiteNow, prefix, prefix, + prefix, g.SQLiteNow, prefix, prefix, + prefix, g.SQLiteNow, prefix, prefix, prefix, prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, g.SQLiteNow, prefix, prefix, g.SQLiteNow, prefix, prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, g.SQLiteNow, prefix, prefix, prefix, prefix, prefix, + prefix, + prefix, + g.SQLiteNow, + prefix, + prefix, + g.SQLiteNow, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + g.SQLiteNow, + prefix, g.SQLiteNow, prefix, ), @@ -612,10 +1108,48 @@ func createTables(db *sql.DB, prefix string) error { }) } +func memberRolesSQL(prefix string) queryT { + const tmpl_read = ` + SELECT role FROM "%s_member_roles" + JOIN "%s_members" ON + "%s_member_roles".member_id = "%s_members".id + WHERE "%s_members".uuid = ? + ORDER BY "%s_member_roles".id; + ` + return queryT{ + read: fmt.Sprintf( + tmpl_read, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), + } +} + +func collectRoles(rows *sql.Rows) ([]string, error) { + roles := []string{} + + for rows.Next() { + var role string + err := rows.Scan(&role) + if err != nil { + rows.Close() + return nil, err + } + + roles = append(roles, role) + } + + return roles, g.WrapErrors(rows.Err(), rows.Close()) +} + func createUserSQL(prefix string) queryT { const tmpl_write = ` INSERT INTO "%s_users" ( - uuid, username, display_name, picture_uuid, deleted + user_uuid, username, display_name, picture_uuid, deleted ) VALUES ( ?, ?, ?, NULL, false ) RETURNING id, timestamp; @@ -673,8 +1207,8 @@ func userByUUIDSQL(prefix string) queryT { picture_uuid FROM "%s_users" WHERE - uuid = ? AND - deleted = false; + user_uuid = ? AND + deleted = false; ` return queryT{ read: fmt.Sprintf(tmpl_read, prefix), @@ -775,8 +1309,8 @@ func deleteUserSQL(prefix string) queryT { UPDATE "%s_users" SET deleted = true WHERE - uuid = ? AND - deleted = false + user_uuid = ? AND + deleted = false RETURNING id; ` return queryT{ @@ -805,46 +1339,86 @@ func deleteUserStmt( func addNetworkSQL(prefix string) queryT { const tmpl_write = ` INSERT INTO "%s_networks" ( - uuid, name, description, type, creator_id + uuid, name, description, type, deleted ) VALUES ( ?, ?, ?, ?, - ( - SELECT id FROM "%s_users" - WHERE id = ? AND deleted = false - ) - ) RETURNING id, timestamp; - + false + ) RETURNING id; + + WITH creator AS ( + SELECT username, display_name, picture_uuid + FROM "%s_users" + WHERE id = ? AND deleted = false + ), new_network AS ( + SELECT id FROM "%s_networks" WHERE uuid = ? + ) INSERT INTO "%s_members" ( - network_id, user_id, username, display_name, + uuid, network_id, user_id, username, display_name, picture_uuid, status, active_uniq ) VALUES ( - last_insert_rowid(), ?, - ( - SELECT username, display_name, picture_uuid - FROM "%s_users" - WHERE id = ? AND deleted = false - ), + (SELECT id FROM new_network), + ?, + (SELECT username FROM creator), + (SELECT display_name FROM creator), + (SELECT picture_uuid FROM creator), 'active', 'active' - ) RETURNING id, timestamp; + ) RETURNING id; + + WITH new_member AS ( + SELECT id FROM "%s_members" WHERE uuid = ? + ) + INSERT INTO "%s_member_roles" (member_id, role) + VALUES ( + (SELECT id FROM new_member), + 'admin' + ), + ( + (SELECT id FROM new_member), + 'creator' + ) + RETURNING id; + ` + const tmpl_read = ` + SELECT id, timestamp FROM "%s_networks" + WHERE uuid = ? AND deleted = false; ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix, prefix), + write: fmt.Sprintf( + tmpl_write, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), + read: fmt.Sprintf(tmpl_read, prefix), } } func addNetworkStmt( cfg dbconfigT, -) (func(userT, newNetworkT) (networkT, error), func() error, error) { +) ( + func(userT, newNetworkT, guuid.UUID) (networkT, error), + func() error, + error, +) { q := addNetworkSQL(cfg.prefix) + readStmt, err := cfg.shared.Prepare(q.read) + if err != nil { + return nil, nil, err + } + privateDB, err := sql.Open(golite.DriverName, cfg.dbpath) if err != nil { + readStmt.Close() return nil, nil, err } @@ -853,10 +1427,10 @@ func addNetworkStmt( fn := func( user userT, newNetwork newNetworkT, + memberID guuid.UUID, ) (networkT, error) { network := networkT{ uuid: newNetwork.uuid, - createdBy: user.uuid, name: newNetwork.name, description: newNetwork.description, type_: newNetwork.type_, @@ -868,87 +1442,35 @@ func addNetworkStmt( newNetwork.description, newNetwork.type_, user.id, + newNetwork.uuid[:], + memberID[:], + user.id, + memberID[:], ) if err != nil { return networkT{}, err } - return network, nil - - /* - member := memberT{ - } - var timestr string - { - FIXME - rows, err := writeStmt.Query( - newNetwork.uuid[:], - newNetwork.name, - newNetwork.description, - newNetwork.type_, - user.id, - ) - if err != nil { - return networkT{}, err - } - defer rows.Close() - - { - if !rows.Next() { - return networkT{}, sql.ErrNoRows - } - - err := rows.Scan(&network.id, ×tr) - if err != nil { - return networkT{}, err - } - - network.timestamp, err = time.Parse( - time.RFC3339Nano, - timestr, - ) - if err != nil { - return networkT{}, err - } - } - - { - if !rows.Next() { - return networkT{}, sql.ErrNoRows - } - - err := rows.Scan(&member.id, ×tr) - if err != nil { - return networkT{}, err - } - - member.timestamp, err = time.Parse( - time.RFC3339Nano, - timestr, - ) - if err != nil { - return networkT{}, err - } - } - - { - if rows.Next() { - return networkT{}, errors.New("FIXME") - } - err := rows.Err() + err = readStmt.QueryRow(network.uuid[:]).Scan( + &network.id, + ×tr, + ) + if err != nil { + return networkT{}, err + } - if err != nil { - return networkT{}, err - } - } + network.timestamp, err = time.Parse(time.RFC3339Nano, timestr) + if err != nil { + return networkT{}, err } - */ + + return network, nil } closeFn := func() error { writeFnClose() - return privateDB.Close() + return g.SomeError(privateDB.Close(), readStmt.Close()) } return fn, closeFn, nil @@ -956,29 +1478,32 @@ func addNetworkStmt( func getNetworkSQL(prefix string) queryT { const tmpl_read = ` + WITH probing_user AS ( + SELECT id FROM "%s_users" + WHERE id = ? AND deleted = false + ), target_network AS ( + SELECT id FROM "%s_networks" + WHERE uuid = ? AND deleted = false + ) SELECT - "%s_networks".id, - "%s_networks".timestamp, - "%s_users".uuid, - "%s_networks".name, - "%s_networks".description, - "%s_networks".type + id, + timestamp, + name, + description, + type FROM "%s_networks" - JOIN "%s_users" ON - "%s_users".id = "%s_networks".creator_id WHERE - "%s_networks".uuid = $networkUUID AND - $userID IN ( - SELECT id FROM "%s_users" - WHERE id = $userID AND deleted = false - ) AND + uuid = ? AND + deleted = false AND + ? IN probing_user AND ( - "%s_networks".type IN ('public', 'unlisted') OR - $userID IN ( + type IN ('public', 'unlisted') OR + ? IN ( SELECT user_id FROM "%s_members" WHERE - user_id = $userID AND - network_id = "%s_networks".id + user_id = ? AND + network_id IN target_network AND + status != 'removed' ) ); ` @@ -1000,6 +1525,13 @@ func getNetworkSQL(prefix string) queryT { prefix, prefix, prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, ), } } @@ -1019,14 +1551,17 @@ func getNetworkStmt( uuid: networkID, } - var ( - timestr string - creator_id_bytes []byte - ) - err := readStmt.QueryRow(networkID[:], user.id).Scan( + var timestr string + err := readStmt.QueryRow( + user.id, + networkID[:], + networkID[:], + user.id, + user.id, + user.id, + ).Scan( &network.id, ×tr, - &creator_id_bytes, &network.name, &network.description, &network.type_, @@ -1034,7 +1569,6 @@ func getNetworkStmt( if err != nil { return networkT{}, err } - network.createdBy = guuid.UUID(creator_id_bytes) network.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { @@ -1057,17 +1591,26 @@ func networkEach(rows *sql.Rows, callback func(networkT) error) error { network networkT timestr string network_id_bytes []byte + deleted bool ) err := rows.Scan( &network.id, ×tr, &network_id_bytes, + &network.name, + &network.description, + &network.type_, + &deleted, ) if err != nil { return g.WrapErrors(rows.Close(), err) } network.uuid = guuid.UUID(network_id_bytes) + if deleted { + return sql.ErrNoRows + } + network.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { return g.WrapErrors(rows.Close(), err) @@ -1084,10 +1627,49 @@ func networkEach(rows *sql.Rows, callback func(networkT) error) error { func networksSQL(prefix string) queryT { const tmpl_read = ` - -- FIXME %s + WITH current_user AS ( + SELECT id, deleted FROM "%s_users" WHERE id = ? + ) + SELECT + "%s_networks".id, + "%s_networks".timestamp, + "%s_networks".uuid, + "%s_networks".name, + "%s_networks".description, + "%s_networks".type, + (SELECT deleted FROM current_user) + FROM "%s_networks" + JOIN "%s_members" ON + "%s_networks".id = "%s_members".network_id + WHERE ( + "%s_networks".type = 'public' OR + "%s_networks".id IN ( + SELECT network_id FROM "%s_members" + WHERE user_id IN (SELECT id FROM current_user) + ) + ) AND "%s_networks".deleted = false + ORDER BY "%s_networks".id; ` return queryT{ - read: fmt.Sprintf(tmpl_read, prefix), + read: fmt.Sprintf( + tmpl_read, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), } } @@ -1102,7 +1684,7 @@ func networksStmt( } fn := func(user userT) (*sql.Rows, error) { - return readStmt.Query(user.uuid[:]) + return readStmt.Query(user.id) } return fn, readStmt.Close, nil @@ -1110,16 +1692,42 @@ func networksStmt( func setNetworkSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + UPDATE "%s_networks" + SET + name = ?, + description = ?, + type = ? + WHERE id = ? AND deleted = false + RETURNING ( + SELECT CASE WHEN EXISTS ( + SELECT role from "%s_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin', + 'network-settings-update' + ) AND ? IN ( + SELECT network_id + FROM "%s_members" + WHERE + id = ? AND + status = 'active' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to update network data' + ) END + ); + ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix), } } func setNetworkStmt( cfg dbconfigT, -) (func(userT, networkT) error, func() error, error) { +) (func(memberT, networkT) error, func() error, error) { q := setNetworkSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1127,9 +1735,17 @@ func setNetworkStmt( return nil, nil, err } - fn := func(user userT, network networkT) error { - _, err := writeStmt.Exec(network) - return err + fn := func(actor memberT, network networkT) error { + var _allowed bool + return writeStmt.QueryRow( + network.name, + network.description, + network.type_, + network.id, + actor.id, + network.id, + actor.id, + ).Scan(&_allowed) } return fn, writeStmt.Close, nil @@ -1137,16 +1753,38 @@ func setNetworkStmt( func nipNetworkSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + WITH target_network AS ( + SELECT network_id AS id + FROM "%s_members" + WHERE + id = ? AND + status = 'active' + ) + UPDATE "%s_networks" + SET deleted = true + WHERE id IN target_network AND deleted = false + RETURNING ( + SELECT CASE WHEN EXISTS ( + SELECT role FROM "%s_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to delete network' + ) END + ); ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix), } } func nipNetworkStmt( cfg dbconfigT, -) (func(userT, guuid.UUID) error, func() error, error) { +) (func(memberT) error, func() error, error) { q := nipNetworkSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1154,27 +1792,192 @@ func nipNetworkStmt( return nil, nil, err } - fn := func(user userT, networkID guuid.UUID) error { - _, err := writeStmt.Exec(networkID[:]) - return err + fn := func(actor memberT) error { + var _allowed bool + return writeStmt.QueryRow(actor.id, actor.id).Scan(&_allowed) } return fn, writeStmt.Close, nil } +func membershipSQL(prefix string) queryT { + const tmpl_read = ` + SELECT + "%s_members".id, + "%s_members".timestamp, + "%s_members".uuid, + "%s_members".username, + "%s_members".display_name, + "%s_members".picture_uuid, + "%s_members".status + FROM "%s_members" + JOIN "%s_users" ON + "%s_users".id = "%s_members".user_id + JOIN "%s_networks" ON + "%s_networks".id = "%s_members".network_id + WHERE + "%s_members".user_id = ? AND + "%s_members".network_id = ? AND + "%s_members".status = 'active' AND + "%s_users".deleted = false AND + "%s_networks".deleted = false; + ` + return queryT{ + read: fmt.Sprintf( + tmpl_read, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), + } +} + +func membershipStmt( + cfg dbconfigT, +) (func(userT, networkT) (memberT, error), func() error, error) { + q := membershipSQL(cfg.prefix) + + readStmt, err := cfg.shared.Prepare(q.read) + if err != nil { + return nil, nil, err + } + + rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read) + if err != nil { + readStmt.Close() + return nil, nil, err + } + + fn := func(actor userT, network networkT) (memberT, error) { + member := memberT{} + + var ( + timestr string + member_id_bytes []byte + picture_id_bytes []byte + ) + err := readStmt.QueryRow(actor.id, network.id).Scan( + &member.id, + ×tr, + &member_id_bytes, + &member.username, + &member.displayName, + &picture_id_bytes, + &member.status, + ) + if err != nil { + return memberT{}, err + } + member.uuid = guuid.UUID(member_id_bytes) + + member.timestamp, err = time.Parse(time.RFC3339Nano, timestr) + if err != nil { + return memberT{}, err + } + + rows, err := rolesStmt.Query(member_id_bytes) + if err != nil { + return memberT{}, err + } + + member.roles, err = collectRoles(rows) + if err != nil { + return memberT{}, err + } + + return member, nil + } + + closeFn := func() error { + return g.SomeError( + readStmt.Close(), + rolesStmt.Close(), + ) + } + + return fn, closeFn, nil +} func addMemberSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + WITH target_user AS ( + SELECT id, username, display_name, picture_uuid + FROM "%s_users" + WHERE user_uuid = ? AND deleted = false + ), target_network AS ( + SELECT "%s_members".network_id AS id + FROM "%s_members" + JOIN "%s_networks" ON + "%s_members".network_id = "%s_networks".id + WHERE + "%s_members".id = ? AND + "%s_members".status = 'active' AND + "%s_networks".deleted = false + ) + INSERT INTO "%s_members" ( + uuid, network_id, user_id, username, display_name, + picture_uuid, status, active_uniq + ) VALUES ( + ?, + (SELECT id FROM target_network), + (SELECT id FROM target_user), + ?, + (SELECT display_name FROM target_user), + (SELECT picture_uuid FROM target_user), + 'active', + 'active' + ) RETURNING id, timestamp, display_name, picture_uuid, status, ( + SELECT CASE WHEN EXISTS ( + SELECT role from "%s_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin', + 'add-member' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to add another member' + ) END + ); ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf( + tmpl_write, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), } } func addMemberStmt( cfg dbconfigT, -) (func(userT, networkT, newMemberT) (memberT, error), func() error, error) { +) (func(memberT, newMemberT) (memberT, error), func() error, error) { q := addMemberSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1182,46 +1985,164 @@ func addMemberStmt( return nil, nil, err } - fn := func( - user userT, - network networkT, - newMember newMemberT, - ) (memberT, error) { + rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read) + if err != nil { + writeStmt.Close() + return nil, nil, err + } + + fn := func(actor memberT, newMember newMemberT) (memberT, error) { member := memberT{ + uuid: newMember.memberID, + username: newMember.username, } - var timestr string - err := writeStmt.QueryRow(network.uuid[:], newMember).Scan( + var ( + timestr string + picture_id_bytes []byte + _allowed bool + ) + err := writeStmt.QueryRow( + newMember.userID[:], + actor.id, + newMember.memberID[:], + newMember.username, + actor.id, + ).Scan( &member.id, ×tr, + &member.displayName, + &picture_id_bytes, + &member.status, + &_allowed, ) if err != nil { return memberT{}, err } + if picture_id_bytes != nil { + pictureID := guuid.UUID(picture_id_bytes) + member.pictureID = &pictureID + } member.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { return memberT{}, err } + rows, err := rolesStmt.Query(member.uuid[:]) + if err != nil { + return memberT{}, err + } + + member.roles, err = collectRoles(rows) + if err != nil { + return memberT{}, err + } + return member, nil } + closeFn := func() error { + return g.SomeError( + writeStmt.Close(), + rolesStmt.Close(), + ) + } + + return fn, closeFn, nil +} + +func addRoleSQL(prefix string) queryT { + const tmpl_write = ` + INSERT INTO "%s_member_roles" (member_id, role) + VALUES (?, ?); + ` + return queryT{ + write: fmt.Sprintf(tmpl_write, prefix), + } +} + +func addRoleStmt( + cfg dbconfigT, +) (func(memberT, string, memberT) error, func() error, error) { + q := addRoleSQL(cfg.prefix) + + writeStmt, err := cfg.shared.Prepare(q.write) + if err != nil { + return nil, nil, err + } + + fn := func(actor memberT, role string, member memberT) error { + // FIXME: do authorization + _, err := writeStmt.Exec(member.id, role) + return err + } + return fn, writeStmt.Close, nil } +func dropRoleSQL(prefix string) queryT { + const tmpl_write = ` + DELETE FROM "%s_member_roles" + WHERE + member_id = ? AND + role = ? + RETURNING 1; + ` + return queryT{ + write: fmt.Sprintf(tmpl_write, prefix), + } +} + +func dropRoleStmt( + cfg dbconfigT, +) (func(memberT, string, memberT) error, func() error, error) { + q := dropRoleSQL(cfg.prefix) + + writeStmt, err := cfg.shared.Prepare(q.write) + if err != nil { + return nil, nil, err + } + + fn := func(actor memberT, role string, member memberT) error { + // FIXME: do authorization + // _, err := writeStmt.Exec(member.id, role) + // return err + var _id int64 + return writeStmt.QueryRow(member.id, role).Scan(&_id) + } + + return fn, writeStmt.Close, nil +} + + func showMemberSQL(prefix string) queryT { const tmpl_read = ` - -- FIXME %s + WITH current_network AS ( + SELECT network_id + FROM "%s_members" + WHERE id = ? + ) + SELECT + id, + timestamp, + username, + display_name, + picture_uuid, + status + FROM "%s_members" + WHERE + uuid = ? AND + network_id IN current_network; ` return queryT{ - read: fmt.Sprintf(tmpl_read, prefix), + read: fmt.Sprintf(tmpl_read, prefix, prefix), } } func showMemberStmt( cfg dbconfigT, -) (func(userT, guuid.UUID) (memberT, error), func() error, error) { +) (func(memberT, guuid.UUID) (memberT, error), func() error, error) { q := showMemberSQL(cfg.prefix) readStmt, err := cfg.shared.Prepare(q.read) @@ -1229,29 +2150,64 @@ func showMemberStmt( return nil, nil, err } - fn := func(user userT, memberID guuid.UUID) (memberT, error) { + rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read) + if err != nil { + readStmt.Close() + return nil, nil, err + } + + fn := func(actor memberT, memberID guuid.UUID) (memberT, error) { member := memberT{ uuid: memberID, } - var timestr string - err := readStmt.QueryRow(memberID[:]).Scan( + var ( + timestr string + picture_id_bytes []byte + ) + err := readStmt.QueryRow(actor.id, memberID[:]).Scan( &member.id, ×tr, + &member.username, + &member.displayName, + &picture_id_bytes, + &member.status, ) if err != nil { return memberT{}, err } + if picture_id_bytes != nil { + pictureID := guuid.UUID(picture_id_bytes) + // FIXME: test this + member.pictureID = &pictureID + } member.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { return memberT{}, err } - return member, err + rows, err := rolesStmt.Query(memberID[:]) + if err != nil { + return memberT{}, err + } + + member.roles, err = collectRoles(rows) + if err != nil { + return memberT{}, err + } + + return member, nil } - return fn, readStmt.Close, nil + closeFn := func() error { + return g.SomeError( + readStmt.Close(), + rolesStmt.Close(), + ) + } + + return fn, closeFn, nil } func memberEach(rows *sql.Rows, callback func(memberT) error) error { @@ -1261,19 +2217,28 @@ func memberEach(rows *sql.Rows, callback func(memberT) error) error { for rows.Next() { var ( - member memberT - timestr string - member_id_bytes []byte + member memberT + timestr string + member_id_bytes []byte + picture_id_bytes []byte ) err := rows.Scan( &member.id, ×tr, &member_id_bytes, + &member.username, + &member.displayName, + &picture_id_bytes, + &member.status, ) if err != nil { return g.WrapErrors(rows.Close(), err) } - // member.uuid = guuid.UUID(member_id_bytes) FIXME + member.uuid = guuid.UUID(member_id_bytes) + if picture_id_bytes != nil { + pictureID := guuid.UUID(picture_id_bytes) + member.pictureID = &pictureID + } member.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { @@ -1291,16 +2256,46 @@ func memberEach(rows *sql.Rows, callback func(memberT) error) error { func membersSQL(prefix string) queryT { const tmpl_read = ` - -- FIXME %s + WITH target_network AS ( + SELECT "%s_members".network_id + FROM "%s_members" + JOIN "%s_networks" ON + "%s_members".network_id = "%s_networks".id + WHERE + "%s_members".id = ? AND + "%s_networks".deleted = false + ) + SELECT + id, + timestamp, + uuid, + username, + display_name, + picture_uuid, + status + FROM "%s_members" + WHERE + network_id IN target_network AND + status = 'active'; ` return queryT{ - read: fmt.Sprintf(tmpl_read, prefix), + read: fmt.Sprintf( + tmpl_read, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ), } } func membersStmt( cfg dbconfigT, -) (func(userT, guuid.UUID) (*sql.Rows, error), func() error, error) { +) (func(memberT) (*sql.Rows, error), func() error, error) { q := membersSQL(cfg.prefix) readStmt, err := cfg.shared.Prepare(q.read) @@ -1308,8 +2303,8 @@ func membersStmt( return nil, nil, err } - fn := func(user userT, networkID guuid.UUID) (*sql.Rows, error) { - return readStmt.Query(networkID[:]) + fn := func(actor memberT) (*sql.Rows, error) { + return readStmt.Query(actor.id) } return fn, readStmt.Close, nil @@ -1317,7 +2312,11 @@ func membersStmt( func editMemberSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + UPDATE "%s_members" + SET + status = ? + WHERE id = ? + RETURNING id; ` return queryT{ write: fmt.Sprintf(tmpl_write, prefix), @@ -1326,7 +2325,7 @@ func editMemberSQL(prefix string) queryT { func editMemberStmt( cfg dbconfigT, -) (func(userT, memberT) error, func() error, error) { +) (func(memberT, memberT) error, func() error, error) { q := editMemberSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1334,9 +2333,12 @@ func editMemberStmt( return nil, nil, err } - fn := func(user userT, member memberT) error { - _, err := writeStmt.Exec(member) - return err + fn := func(actor memberT, member memberT) error { + var _id int64 + return writeStmt.QueryRow( + member.status, + member.id, + ).Scan(&_id) } return fn, writeStmt.Close, nil @@ -1344,59 +2346,115 @@ func editMemberStmt( func dropMemberSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME + UPDATE "%s_members" SET status = 'removed' + WHERE uuid = ? RETURNING id; + + DELETE FROM "%s_member_roles" + WHERE + role != 'creator' AND + member_id IN ( + SELECT id FROM "%s_members" + WHERE uuid = ? + ) ` return queryT{ - write: fmt.Sprintf(tmpl_write), + write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix), } } func dropMemberStmt( cfg dbconfigT, -) (func(userT, guuid.UUID) error, func() error, error) { +) (func(memberT, guuid.UUID) error, func() error, error) { q := dropMemberSQL(cfg.prefix) - writeStmt, err := cfg.shared.Prepare(q.write) + privateDB, err := sql.Open(golite.DriverName, cfg.dbpath) if err != nil { return nil, nil, err } - fn := func(user userT, memberID guuid.UUID) error { - _, err := writeStmt.Exec(memberID[:]) - return err + writeFn, writeFnClose := execSerialized(q.write, privateDB) + + fn := func(actor memberT, memberID guuid.UUID) error { + err := writeFn(memberID[:], memberID[:]) + if err != nil { + return err + } + + // if res == 0 { // FIXME } + return nil } - return fn, writeStmt.Close, nil + closeFn := func() error { + writeFnClose() + return privateDB.Close() + } + + return fn, closeFn, nil } func addChannelSQL(prefix string) queryT { const tmpl_write = ` + WITH target_network AS ( + SELECT network_id AS id + FROM "%s_members" + WHERE id = ? + ) INSERT INTO "%s_channels" ( - uuid, public_name, label, description, virtual - ) VALUES (?, ?, ?, ?, ?) RETURNING id, timestamp; + uuid, + network_id, + public_name, + label, + description, + virtual + ) VALUES ( + ?, + (SELECT id FROM target_network), + ?, + ?, + ?, + ? + ) RETURNING id, timestamp; + + WITH new_channel AS ( + SELECT id FROM "%s_channels" WHERE uuid = ? + ) + INSERT INTO "%s_participants" (channel_id, member_id) + VALUES ( + (SELECT id FROM new_channel), + ? + ); + ` + const tmpl_read = ` + SELECT id, timestamp FROM "%s_channels" + WHERE uuid = ?; ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix, prefix), + read: fmt.Sprintf(tmpl_read, prefix), } } func addChannelStmt( cfg dbconfigT, -) (func (guuid.UUID, newChannelT) (channelT, error), func() error, error) { +) (func (memberT, newChannelT) (channelT, error), func() error, error) { q := addChannelSQL(cfg.prefix) - writeStmt, err := cfg.shared.Prepare(q.write) + readStmt, err := cfg.shared.Prepare(q.read) if err != nil { return nil, nil, err } - fn := func( - networkID guuid.UUID, - newChannel newChannelT, - ) (channelT, error) { + privateDB, err := sql.Open(golite.DriverName, cfg.dbpath) + if err != nil { + readStmt.Close() + return nil, nil, err + } + + writeFn, writeFnClose := execSerialized(q.write, privateDB) + + fn := func(actor memberT, newChannel newChannelT) (channelT, error) { channel := channelT{ uuid: newChannel.uuid, - // networkID[:], publicName: newChannel.publicName, label: newChannel.label, description: newChannel.description, @@ -1404,13 +2462,24 @@ func addChannelStmt( } var timestr string - err := writeStmt.QueryRow( + err := writeFn( + actor.id, newChannel.uuid[:], newChannel.publicName, newChannel.label, newChannel.description, newChannel.virtual, - ).Scan(&channel.id, ×tr) + newChannel.uuid[:], + actor.id, + ) + if err != nil { + return channelT{}, err + } + + err = readStmt.QueryRow(newChannel.uuid[:]).Scan( + &channel.id, + ×tr, + ) if err != nil { return channelT{}, err } @@ -1423,9 +2492,35 @@ func addChannelStmt( return channel, nil } - return fn, writeStmt.Close, nil + closeFn := func() error { + writeFnClose() + return readStmt.Close() + } + + return fn, closeFn, nil } +/* +func chanByName(prefix string) queryT { + const tmpl_read = ` + SELECT + id, + timestamp, + uuid, + public, + + ` + return queryT{ + read: fmt.Sprintf(tmpl_read, prefix), + } +} + +func chanByStmt( + cfg dbconfigT, +) ( +FIXME +*/ + func channelEach(rows *sql.Rows, callback func(channelT) error) error { if rows == nil { return nil @@ -1436,16 +2531,24 @@ func channelEach(rows *sql.Rows, callback func(channelT) error) error { channel channelT timestr string channel_id_bytes []byte + publicName sql.NullString ) err := rows.Scan( &channel.id, ×tr, &channel_id_bytes, + &publicName, + &channel.label, + &channel.description, + &channel.virtual, ) if err != nil { return g.WrapErrors(rows.Close(), err) } channel.uuid = guuid.UUID(channel_id_bytes) + if publicName.Valid { + channel.publicName = &publicName.String + } channel.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { @@ -1463,16 +2566,40 @@ func channelEach(rows *sql.Rows, callback func(channelT) error) error { func channelsSQL(prefix string) queryT { const tmpl_read = ` - -- FIXME %s + WITH current_network AS ( + SELECT network_id AS id + FROM "%s_members" + WHERE id = ? + ), member_private_channels AS ( + SELECT channel_id AS id + FROM "%s_participants" + WHERE member_id = ? + ) + SELECT + id, + timestamp, + uuid, + public_name, + label, + description, + virtual + FROM "%s_channels" + WHERE + network_id IN current_network AND + ( + public_name IS NOT NULL OR + id IN member_private_channels + ) + ORDER BY id; ` return queryT{ - read: fmt.Sprintf(tmpl_read, prefix), + read: fmt.Sprintf(tmpl_read, prefix, prefix, prefix), } } func channelsStmt( cfg dbconfigT, -) (func(guuid.UUID) (*sql.Rows, error), func() error, error) { +) (func(memberT) (*sql.Rows, error), func() error, error) { q := channelsSQL(cfg.prefix) readStmt, err := cfg.shared.Prepare(q.read) @@ -1480,38 +2607,96 @@ func channelsStmt( return nil, nil, err } - fn := func(networkID guuid.UUID) (*sql.Rows, error) { - return readStmt.Query(networkID[:]) + fn := func(actor memberT) (*sql.Rows, error) { + return readStmt.Query(actor.id, actor.id) } return fn, readStmt.Close, nil } -func topicSQL(prefix string) queryT { +func setChannelSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + WITH participant_channel AS ( + SELECT channel_id AS id + FROM "%s_participants" + WHERE + member_id = ? AND + channel_id = ? + ) + UPDATE "%s_channels" + SET + description = ?, + public_name = ? + WHERE id IN participant_channel + RETURNING id; + ` + const tmpl_read = ` + SELECT ( + SELECT network_id AS id + FROM "%s_channels" + WHERE id = ? + ) AS channel_network_id, ( + SELECT network_id AS id + FROM "%s_members" + WHERE id = ? + ) AS member_network_id; ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix), + read: fmt.Sprintf(tmpl_read, prefix, prefix), } } -func topicStmt( +func setChannelStmt( cfg dbconfigT, -) (func(channelT) error, func() error, error) { - q := topicSQL(cfg.prefix) +) (func(memberT, channelT) error, func() error, error) { + q := setChannelSQL(cfg.prefix) + + readStmt, err := cfg.shared.Prepare(q.read) + if err != nil { + return nil, nil, err + } writeStmt, err := cfg.shared.Prepare(q.write) if err != nil { + readStmt.Close() return nil, nil, err } - fn := func(channel channelT) error { - _, err := writeStmt.Exec(channel) - return err + fn := func(actor memberT, channel channelT) error { + var ( + netid1 sql.NullInt64 + netid2 sql.NullInt64 + ) + err := readStmt.QueryRow(channel.id, actor.id).Scan( + &netid1, + &netid2, + ) + if err != nil { + return err + } + if !netid1.Valid || !netid2.Valid || + netid1.Int64 != netid2.Int64 { + return sql.ErrNoRows + } + + var _id int64 + return writeStmt.QueryRow( + actor.id, + channel.id, + channel.description, + channel.publicName, + ).Scan(&_id) } - return fn, writeStmt.Close, nil + closeFn := func() error { + return g.SomeError( + readStmt.Close(), + writeStmt.Close(), + ) + } + + return fn, closeFn, nil } func endChannelSQL(prefix string) queryT { @@ -1525,7 +2710,7 @@ func endChannelSQL(prefix string) queryT { func endChannelStmt( cfg dbconfigT, -) (func(guuid.UUID) error, func() error, error) { +) (func(memberT, guuid.UUID) error, func() error, error) { q := endChannelSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1533,7 +2718,7 @@ func endChannelStmt( return nil, nil, err } - fn := func(channelID guuid.UUID) error { + fn := func(actor memberT, channelID guuid.UUID) error { _, err := writeStmt.Exec(channelID[:]) return err } @@ -1543,43 +2728,108 @@ func endChannelStmt( func joinSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + WITH target_channel AS ( + SELECT id + FROM "%s_channels" + WHERE + uuid = ? AND + public_name IS NOT NULL + ) + INSERT INTO "%s_participants" (channel_id, member_id) + VALUES ( + (SELECT id FROM target_channel), + ? + ) RETURNING id; + ` + const tmpl_read = ` + SELECT ( + SELECT network_id AS id + FROM "%s_channels" + WHERE + uuid = ? AND + public_name IS NOT NULL + ) AS channel_network_id, ( + SELECT network_id AS id + FROM "%s_members" WHERE id = ? + ) AS member_network_id; ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix), + read: fmt.Sprintf(tmpl_read, prefix, prefix), } } func joinStmt( cfg dbconfigT, -) (func(guuid.UUID, guuid.UUID) error, func() error, error) { +) (func(memberT, guuid.UUID) error, func() error, error) { q := joinSQL(cfg.prefix) + readStmt, err := cfg.shared.Prepare(q.read) + if err != nil { + return nil, nil, err + } + writeStmt, err := cfg.shared.Prepare(q.write) if err != nil { + readStmt.Close() return nil, nil, err } - fn := func(memberID guuid.UUID, channelID guuid.UUID) error { - _, err := writeStmt.Exec(memberID[:], channelID[:]) - return err + fn := func(actor memberT, channelID guuid.UUID) error { + var ( + netid1 sql.NullInt64 + netid2 sql.NullInt64 + ) + err := readStmt.QueryRow(channelID[:], actor.id).Scan( + &netid1, + &netid2, + ) + if err != nil { + return err + } + + if !netid1.Valid || !netid2.Valid || + netid1.Int64 != netid2.Int64 { + return sql.ErrNoRows + } + + var _id int64 + return writeStmt.QueryRow(channelID[:], actor.id).Scan(&_id) } - return fn, writeStmt.Close, nil + closeFn := func() error { + return g.SomeError( + readStmt.Close(), + writeStmt.Close(), + ) + } + + return fn, closeFn, nil } func partSQL(prefix string) queryT { const tmpl_write = ` - -- FIXME %s + WITH target_channel AS ( + SELECT id + FROM "%s_channels" + WHERE + id = ? AND + virtual = false + ) + DELETE FROM "%s_participants" + WHERE + member_id = ? AND + channel_id IN target_channel + RETURNING 1; ` return queryT{ - write: fmt.Sprintf(tmpl_write, prefix), + write: fmt.Sprintf(tmpl_write, prefix, prefix), } } func partStmt( cfg dbconfigT, -) (func(guuid.UUID, guuid.UUID) error, func() error, error) { +) (func(memberT, channelT) error, func() error, error) { q := partSQL(cfg.prefix) writeStmt, err := cfg.shared.Prepare(q.write) @@ -1587,9 +2837,9 @@ func partStmt( return nil, nil, err } - fn := func(memberID guuid.UUID, channelID guuid.UUID) error { - _, err := writeStmt.Exec(memberID[:], channelID[:]) - return err + fn := func(actor memberT, channel channelT) error { + var _id int64 + return writeStmt.QueryRow(channel.id, actor.id).Scan(&_id) } return fn, writeStmt.Close, nil @@ -1641,7 +2891,7 @@ func namesSQL(prefix string) queryT { func namesStmt( cfg dbconfigT, -) (func(guuid.UUID) (*sql.Rows, error), func() error, error) { +) (func(memberT, guuid.UUID) (*sql.Rows, error), func() error, error) { q := namesSQL(cfg.prefix) readStmt, err := cfg.shared.Prepare(q.read) @@ -1649,7 +2899,7 @@ func namesStmt( return nil, nil, err } - fn := func(channelID guuid.UUID) (*sql.Rows, error) { + fn := func(actor memberT, channelID guuid.UUID) (*sql.Rows, error) { return readStmt.Query(channelID[:]) } @@ -1659,12 +2909,16 @@ func namesStmt( func addEventSQL(prefix string) queryT { const tmpl_write = ` INSERT INTO "%s_channel_events" ( - uuid, channel_id, connection_uuid, type, payload + uuid, channel_id, source_uuid, source_type, + source_metadata, type, payload, metadata ) VALUES ( ?, (SELECT id FROM "%s_channels" WHERE uuid = ?), ?, ?, + ?, + ?, + ?, ? ) RETURNING id, timestamp; ` @@ -1685,20 +2939,24 @@ func addEventStmt( fn := func(newEvent newEventT) (eventT, error) { event := eventT{ - uuid: newEvent.eventID, - channelID: newEvent.channelID, - connectionID: newEvent.connectionID, - type_: newEvent.type_, - payload: newEvent.payload, + uuid: newEvent.eventID, + channelID: newEvent.channelID, + source: newEvent.source, + type_: newEvent.type_, + payload: newEvent.payload, + metadata: newEvent.metadata, } var timestr string err := writeStmt.QueryRow( newEvent.eventID[:], newEvent.channelID[:], - newEvent.connectionID[:], + newEvent.source.uuid[:], + newEvent.source.type_, + newEvent.source.metadata, newEvent.type_, newEvent.payload, + newEvent.metadata, ).Scan(&event.id, ×tr) if err != nil { return eventT{}, err @@ -1726,14 +2984,12 @@ func eventEach(rows *sql.Rows, callback func(eventT) error) error { timestr string event_id_bytes []byte channel_id_bytes []byte - connection_id_bytes []byte ) err := rows.Scan( &event.id, ×tr, &event_id_bytes, &channel_id_bytes, - &connection_id_bytes, &event.type_, &event.payload, ) @@ -1743,7 +2999,6 @@ func eventEach(rows *sql.Rows, callback func(eventT) error) error { } event.uuid = guuid.UUID(event_id_bytes) event.channelID = guuid.UUID(channel_id_bytes) - event.connectionID = guuid.UUID(connection_id_bytes) event.timestamp, err = time.Parse(time.RFC3339Nano, timestr) if err != nil { @@ -1773,7 +3028,7 @@ func allAfterSQL(prefix string) queryT { "%s_channel_events".timestamp, "%s_channel_events".uuid, "%s_channels".uuid, - "%s_channel_events".connection_uuid, + -- "%s_channel_events".connection_uuid, "%s_channel_events".type, "%s_channel_events".payload FROM "%s_channel_events" @@ -1808,7 +3063,7 @@ func allAfterSQL(prefix string) queryT { func allAfterStmt( cfg dbconfigT, -) (func (guuid.UUID) (*sql.Rows, error), func() error, error) { +) (func (memberT, guuid.UUID) (*sql.Rows, error), func() error, error) { q := allAfterSQL(cfg.prefix) readStmt, err := cfg.shared.Prepare(q.read) @@ -1816,7 +3071,7 @@ func allAfterStmt( return nil, nil, err } - fn := func(eventID guuid.UUID) (*sql.Rows, error) { + fn := func(actor memberT, eventID guuid.UUID) (*sql.Rows, error) { return readStmt.Query(eventID[:]) } @@ -1842,6 +3097,7 @@ func logMessageStmt( return nil, nil, err } + // FIXME: actor? fn := func(user userT, message messageT) error { return nil // FIXME _, err := writeStmt.Exec(user, message) @@ -1881,14 +3137,17 @@ func initDB( networks, networksClose, networksErr := networksStmt(cfg) setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg) nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addRole, addRoleClose, addRoleErr := addRoleStmt(cfg) + dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg) showMember, showMemberClose, showMemberErr := showMemberStmt(cfg) members, membersClose, membersErr := membersStmt(cfg) editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg) addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) channels, channelsClose, channelsErr := channelsStmt(cfg) - topic, topicClose, topicErr := topicStmt(cfg) + setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg) endChannel, endChannelClose, endChannelErr := endChannelStmt(cfg) join, joinClose, joinErr := joinStmt(cfg) part, partClose, partErr := partStmt(cfg) @@ -1908,14 +3167,17 @@ func initDB( networksClose, setNetworkClose, nipNetworkClose, + membershipClose, addMemberClose, + addRoleClose, + dropRoleClose, showMemberClose, membersClose, editMemberClose, dropMemberClose, addChannelClose, channelsClose, - topicClose, + setChannelClose, endChannelClose, joinClose, partClose, @@ -1937,14 +3199,17 @@ func initDB( networksErr, setNetworkErr, nipNetworkErr, + membershipErr, addMemberErr, + addRoleErr, + dropRoleErr, showMemberErr, membersErr, editMemberErr, dropMemberErr, addChannelErr, channelsErr, - topicErr, + setChannelErr, endChannelErr, joinErr, partErr, @@ -1954,12 +3219,6 @@ func initDB( logMessageErr, ) if err != nil { - ferr := g.SomeError( - - createUserErr, - - ) - fmt.Printf("ferr: %#v\n", ferr) closeFn() return queriesT{}, err } @@ -1986,10 +3245,14 @@ func initDB( defer connMutex.RUnlock() return deleteUser(a) }, - addNetwork: func(a userT, b newNetworkT) (networkT, error) { + addNetwork: func( + a userT, + b newNetworkT, + c guuid.UUID, + ) (networkT, error) { connMutex.RLock() defer connMutex.RUnlock() - return addNetwork(a, b) + return addNetwork(a, b, c) }, getNetwork: func(a userT, b guuid.UUID) (networkT, error) { connMutex.RLock() @@ -2015,35 +3278,42 @@ func initDB( return networkEach(rows, callback) }, - setNetwork: func(a userT, b networkT) error { + setNetwork: func(a memberT, b networkT) error { connMutex.RLock() defer connMutex.RUnlock() return setNetwork(a, b) }, - nipNetwork: func(a userT, b guuid.UUID) error { + nipNetwork: func(a memberT) error { connMutex.RLock() defer connMutex.RUnlock() - return nipNetwork(a, b) + return nipNetwork(a) }, - addMember: func( - a userT, - b networkT, - c newMemberT, - ) (memberT, error) { + membership: func(a userT, b networkT) (memberT, error) { + connMutex.RLock() + defer connMutex.RUnlock() + return membership(a, b) + }, + addMember: func(a memberT, b newMemberT) (memberT, error) { + connMutex.RLock() + defer connMutex.RUnlock() + return addMember(a, b) + }, + addRole: func(a memberT, b string, c memberT) error { connMutex.RLock() defer connMutex.RUnlock() - return addMember(a, b, c) + return addRole(a, b, c) }, - showMember: func(a userT, b guuid.UUID) (memberT, error) { + dropRole: func(a memberT, b string, c memberT) error { + connMutex.RLock() + defer connMutex.RUnlock() + return dropRole(a, b, c) + }, + showMember: func(a memberT, b guuid.UUID) (memberT, error) { connMutex.RLock() defer connMutex.RUnlock() return showMember(a, b) }, - members: func( - a userT, - b guuid.UUID, - callback func(memberT) error, - ) error { + members: func(a memberT, callback func(memberT) error) error { var ( err error rows *sql.Rows @@ -2051,7 +3321,7 @@ func initDB( { connMutex.RLock() defer connMutex.RUnlock() - rows, err = members(a, b) + rows, err = members(a) } if err != nil { return err @@ -2059,25 +3329,25 @@ func initDB( return memberEach(rows, callback) }, - editMember: func(a userT, b memberT) error { + editMember: func(a memberT, b memberT) error { connMutex.RLock() defer connMutex.RUnlock() return editMember(a, b) }, - dropMember: func(a userT, b guuid.UUID) error { + dropMember: func(a memberT, b guuid.UUID) error { connMutex.RLock() defer connMutex.RUnlock() return dropMember(a, b) }, addChannel: func( - a guuid.UUID, b newChannelT, + a memberT, b newChannelT, ) (channelT, error) { connMutex.RLock() defer connMutex.RUnlock() return addChannel(a, b) }, channels: func( - a guuid.UUID, + a memberT, callback func(channelT) error, ) error { var ( @@ -2095,27 +3365,31 @@ func initDB( return channelEach(rows, callback) }, - topic: func(a channelT) error { + setChannel: func(a memberT, b channelT) error { connMutex.RLock() defer connMutex.RUnlock() - return topic(a) + return setChannel(a, b) }, - endChannel: func(a guuid.UUID) error { + endChannel: func(a memberT, b guuid.UUID) error { connMutex.RLock() defer connMutex.RUnlock() - return endChannel(a) + return endChannel(a, b) }, - join: func(a guuid.UUID, b guuid.UUID) error { + join: func(a memberT, b guuid.UUID) error { connMutex.RLock() defer connMutex.RUnlock() return join(a, b) }, - part: func(a guuid.UUID, b guuid.UUID) error { + part: func(a memberT, b channelT) error { connMutex.RLock() defer connMutex.RUnlock() return part(a, b) }, - names: func(a guuid.UUID, callback func(memberT) error) error { + names: func( + a memberT, + b guuid.UUID, + callback func(memberT) error, + ) error { var ( err error rows *sql.Rows @@ -2123,7 +3397,7 @@ func initDB( { connMutex.RLock() defer connMutex.RUnlock() - rows, err = names(a) + rows, err = names(a, b) } if err != nil { return err @@ -2137,7 +3411,8 @@ func initDB( return addEvent(a) }, allAfter: func( - a guuid.UUID, + a memberT, + b guuid.UUID, callback func(eventT) error, ) error { var ( @@ -2147,7 +3422,7 @@ func initDB( { connMutex.RLock() defer connMutex.RUnlock() - rows, err = allAfter(a) + rows, err = allAfter(a, b) } if err != nil { return err @@ -2236,19 +3511,110 @@ func initListeners( }, nil } -func makeReceivers() receiversT { - var rwmutex sync.Mutex - return receiversT{ - add: func(receiver receiverT) { +type stateT struct{ + connected func(*connectionT) + disconnect func(*connectionT) + authenticated func(*connectionT) + subscribe func(string, []string) + members func(string) []string + connections func(string) []guuid.UUID + connection func(guuid.UUID) *connectionT +} + +// TODO: key for members should be the channelID, not its name +type stateDataT struct{ + connections map[guuid.UUID]*connectionT + users map[string][]guuid.UUID + members map[string]map[string][]guuid.UUID +} + +// TODO: lock is global, should be by network +func newState() stateT { + var rwmutex sync.RWMutex + state := stateDataT{ + connections: map[guuid.UUID]*connectionT{}, + users: map[string][]guuid.UUID{}, + members: map[string]map[string][]guuid.UUID{}, + } + return stateT{ + connected: func(connection *connectionT) { + rwmutex.Lock() + defer rwmutex.Unlock() + state.connections[connection.uuid] = connection }, - remove: func(receiver receiverT) { + disconnect: func(connection *connectionT) { + { + rwmutex.Lock() + defer rwmutex.Unlock() + delete(state.connections, connection.uuid) + delete(state.users, connection.user.username) + delete(state.members, connection.user.username) + } + err := connection.conn.Close() + if err != nil { + g.Warning( + "Failed to close the connection", + "close-error", + "from", "daemon", + "err", err, + ) + } }, - get: func(guuid.UUID) []receiverT{ - return nil + authenticated: func(connection *connectionT) { + username := connection.user.username + rwmutex.Lock() + defer rwmutex.Unlock() + if state.users[username] == nil { + state.users[username] = []guuid.UUID{} + } + state.users[username] = append( + state.users[username], + connection.uuid, + ) }, - close: func() { + subscribe: func( + username string, + channelNames []string, + ) { rwmutex.Lock() defer rwmutex.Unlock() + for _, channelName := range channelNames { + if state.members[channelName] == nil { + state.members[channelName] = + map[string][]guuid.UUID{} + } + state.members[channelName][username] = + state.users[username] + } + }, + members: func(channelName string) []string { + rwmutex.RLock() + defer rwmutex.RUnlock() + usernames := make( + []string, + len(state.members[channelName]), + ) + i := 0 + for username, _ := range state.members[channelName] { + usernames[i] = username + i++ + } + return usernames + }, + connections: func(username string) []guuid.UUID { + rwmutex.RLock() + defer rwmutex.RUnlock() + connections := make( + []guuid.UUID, + len(state.users[username]), + ) + copy(connections, state.users[username]) + return connections + }, + connection: func(connectionID guuid.UUID) *connectionT { + rwmutex.RLock() + defer rwmutex.RUnlock() + return state.connections[connectionID] }, } } @@ -2311,7 +3677,8 @@ func NewWithPrefix( } consumers := buildConsumers(prefix) - receivers := makeReceivers() + state := newState() + // receivers := makeReceivers() metrics := buildMetrics(prefix) // logger := g.NewLogger("prefix", prefix, "program", "papod") @@ -2321,7 +3688,8 @@ func NewWithPrefix( queries: queries, listeners: listeners, consumers: consumers, - receivers: receivers, + state: state, + // receivers: receivers, metrics: metrics, // logger: logger, }, nil @@ -2355,29 +3723,36 @@ func splitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) { return advance, token, error } +func splitCommas(r rune) bool { + return r == ',' +} + func splitSpaces(r rune) bool { return r == ' ' } -func parseMessageParams(params string) messageParamsT { +func parseMessageParams(params string) []string { const sep = " :" - var middle string - var trailing string - idx := strings.Index(params, sep) if idx == -1 { - middle = params - trailing = "" + return strings.FieldsFunc(params, splitSpaces) } else { - middle = params[:idx] - trailing = params[idx + len(sep):] + middle := params[:idx] + trailing := params[idx + len(sep):] + return append( + strings.FieldsFunc(middle, splitSpaces), + trailing, + ) } +} - return messageParamsT{ - middle: strings.FieldsFunc(middle, splitSpaces), - trailing: trailing, +func stripBlankParams(params []string) []string { + if len(params) == 1 && len(params[0]) == 0 { + return []string{} } + + return params } var messageRegex = regexp.MustCompilePOSIX( @@ -2396,7 +3771,7 @@ func parseMessage(rawMessage string) (messageT, error) { msg = messageT{ prefix: components[2], command: components[3], - params: parseMessageParams(components[4]), + params: stripBlankParams(parseMessageParams(components[4])), raw: rawMessage, } return msg, nil @@ -2418,332 +3793,470 @@ func asReply(event eventT) replyT { return replyT{} } -func broadcastEvent(event eventT, receiversFn func(guuid.UUID) []receiverT) { - message := asMessage(event) - for _, receiver := range receiversFn(event.channelID) { - // FIXME: - // is this death by a thousand goroutines? Is the runtime - // able to handle the creation and destruction of hundreds of - // thousands of goroutines per second? - go receiver.send(message) + +/// Is this death by a thousand goroutines? Is the runtime able to handle the +/// creation and destruction of hundreds of thousands of goroutines per second? +/// For now, we'll assume that Go's (gc) runtime, scheduler and garbage +/// collector are capable of working together to make sure this isn't a +/// catastrophe. +func broadcastMessage( + message messageT, + channelName string, + usersFn func(string) []string, + connectionIDsFn func(string) []guuid.UUID, + connectionFn func(guuid.UUID) *connectionT, +) { + for _, username := range usersFn(channelName) { + for _, connectionID := range connectionIDsFn(username) { + connection := connectionFn(connectionID) + if connection == nil { + continue + } + + go connection.send(message) + } } } -var ( - replyErrUnknown = replyT{ - command: "421", - params: messageParamsT{ - middle: []string{}, - trailing: "Unknown command", + +/* +Intentionally not implemented: + +- RPL_BOUNCE + +*/ + +// FIXME: add check for minRPL... +const minRPL_WELCOME = 0 +func _RPL_WELCOME(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "001", + params: []string{ + connection.user.username, + "Welcome to the Internet Relay Network " + + connection.user.username, }, } - replyErrNotRegistered = replyT{ - command: "451", - params: messageParamsT{ - middle: []string{}, - trailing: "You have not registered", +} + +const minRPL_YOURHOST = 0 +func _RPL_YOURHOST(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "002", + params: []string{ + connection.user.username, + "Your host is FIXME, running version " + + Version, + }, + } +} + +const minRPL_CREATED = 0 +func _RPL_CREATED(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "003", + params: []string{ + connection.user.username, + "This server was create FIXME", + }, + } +} + +const minRPL_MYINFO = 0 +func _RPL_MYINFO(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "004", + params: []string{ + connection.user.username, + "FIXME " + Version + " i x", }, } - replyErrFileError = replyT{ +} + +const minRPL_UNAWAY = 0 +func _RPL_UNAWAY(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "305", + params: []string{ + connection.user.username, + "You are no longer marked as away", + }, + } +} + +const minRPL_NOWAWAY = 0 +func _RPL_NOWAWAY(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "306", + params: []string{ + connection.user.username, + "You have been marked as away", + }, + } +} + +const minRPL_WHOISUSER = 1 +func _RPL_WHOISUSER(connection *connectionT, msg messageT) replyT { + user := msg.params[0] + return replyT{ + command: "311", + params: []string{ + connection.user.username, + user, + user, + "samehost", + "*", + "my real name is: " + user, + }, + } +} + +const minRPL_WHOISSERVER = 1 +func _RPL_WHOISSERVER(connection *connectionT, msg messageT) replyT { + user := msg.params[0] + return replyT{ + command: "312", + params: []string{ + connection.user.username, + user, + "stillsamehost", + "some server info", + }, + } +} + +const minRPL_ENDOFWHOIS = 1 +func _RPL_ENDOFWHOIS(connection *connectionT, msg messageT) replyT { + user := msg.params[0] + return replyT{ + command: "318", + params: []string{ + connection.user.username, + user, + "End of WHOIS list", + }, + } +} + +const minRPL_WHOISCHANNELS = 1 +func _RPL_WHOISCHANNELS(connection *connectionT, msg messageT) replyT { + user := msg.params[0] + return replyT{ + command: "319", + params: []string{ + connection.user.username, + user, + "#default", + }, + } +} + +const minRPL_CHANNELMODEIS = 1 +func _RPL_CHANNELMODEIS(connection *connectionT, msg messageT) replyT { + channel := msg.params[0] + return replyT{ + command: "324", + params: []string{ + connection.user.username, + channel, + "+Cnst", + }, + } +} + +const minRPL_NOTOPIC = 1 +func _RPL_NOTOPIC(connection *connectionT, msg messageT) replyT { + channel := msg.params[0] + return replyT{ + command: "331", + params: []string{ + connection.user.username, + channel, + "No topic is set", + }, + } +} + +const minRPL_NAMREPLY = 1 +func _RPL_NAMREPLY(connection *connectionT, msg messageT) replyT { + channel := msg.params[0] + return replyT{ + command: "353", + params: []string{ + connection.user.username, + "=", + channel, + connection.user.username + " virtualuser", + }, + } +} + +const minRPL_ENDOFNAMES = 1 +func _RPL_ENDOFNAMES(connection *connectionT, msg messageT) replyT { + channel := msg.params[0] + return replyT{ + command: "366", + params: []string{ + connection.user.username, + channel, + "End of NAMES list", + }, + } +} + +const minERR_UNKNOWNCOMMAND = 0 +func _ERR_UNKNOWNCOMMAND(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "421", + params: []string{ + connection.user.username, + "Unknown command", + }, + } +} + +const minERR_FILEERROR = 0 +func _ERR_FILEERROR(connection *connectionT, msg messageT) replyT { + return replyT{ command: "424", - params: messageParamsT{ - middle: []string{}, - trailing: "File error doing query on database", + params: []string{ + "File error doing query on database", }, } - RPL_WELCOME = replyT{ - command: "001", - params: messageParamsT{ - middle: []string{}, - trailing: "", +} + +const minERR_NOTREGISTERED = 0 +func _ERR_NOTREGISTERED(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "451", + params: []string{ + "You have not registered", }, } -) +} -func handleUnknown( - papod papodT, - connection *connectionT, - msg messageT, -) ([]replyT, error) { - // FIXME: user doesn't exist when unauthenticated - err := papod.queries.logMessage(userT{ }, msg) - if err != nil { - g.Warning( - "Failed to log message", fmt.Sprintf("%#v", msg), - "group-as", "db-write", - "handler-action", "log-and-ignore", - "connection", connection.uuid.String(), - "err", err, - ) +const minERR_NEEDMOREPARAMS = 0 +func _ERR_NEEDMOREPARAMS(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "461", + params: []string{ + msg.command, + "Not enough parameters", + }, } +} + - return []replyT{ replyErrUnknown }, nil +func _CAP(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "CAP", + params: []string { + "*", + "LS", + }, + } +} + +const minPONG = 0 +func _PONG(connection *connectionT, msg messageT) replyT { + return replyT{ + command: "PONG", + params: msg.params, + } } + +const minUSER = 4 func handleUSER( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - u := connection.user.username - m := []string{ u } - return []replyT{ replyT{ - command: "001", - params: messageParamsT{ - middle: m, - trailing: "Welcome to the Internet Relay Network " + u, - }, - }, replyT{ - command: "002", - params: messageParamsT{ - middle: m, - trailing: "Your host is FIXME, running version " + - Version, - }, - }, replyT{ - command: "003", - params: messageParamsT{ - middle: m, - trailing: "This server was create FIXME", - }, - }, replyT{ - command: "004", - params: messageParamsT{ - middle: m, - trailing: "FIXME " + Version + " i x", - }, - }, }, nil +) ([]replyT, bool, error) { + return []replyT{ + _RPL_WELCOME (connection, msg), + _RPL_YOURHOST(connection, msg), + _RPL_CREATED (connection, msg), + _RPL_MYINFO (connection, msg), + }, false, nil } +const minNICK = 1 func handleNICK( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - connection.user.username = msg.params.middle[0] - return []replyT{}, nil +) ([]replyT, bool, error) { + connection.user.username = msg.params[0] + return []replyT{}, false, nil } +const minPRIVMSG = 2 func handlePRIVMSG( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - // FIXME: check missing params +) ([]replyT, bool, error) { // FIXME: check if user is member of channel, and is authorized to post // FIXME: adapt to handle multiple targets - return []replyT{}, nil - - event, err := papod.queries.addEvent(asNewEvent(msg)) - if err != nil { - // FIXME: not allowed reply per RFC 1459, check other specs - return []replyT{ replyErrFileError }, nil - } - - go broadcastEvent(event, papod.receivers.get) - - reply := asReply(event) - return []replyT{ reply }, nil + go broadcastMessage( + msg, + msg.params[0], + papod.state.members, + papod.state.connections, + papod.state.connection, + ) + return []replyT{}, false, nil } +const minTOPIC = 2 func handleTOPIC( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - return []replyT{ replyT{ - command: "JOIN", - params: messageParamsT{ - middle: []string{ msg.params.middle[0] }, - trailing: "", - }, - } }, nil +) ([]replyT, bool, error) { + return []replyT{ + _RPL_NOTOPIC(connection, msg), + }, false, nil } +const minJOIN = 1 func handleJOIN( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - u := connection.user.username - channel := msg.params.middle[0] - return []replyT{ replyT{ - command: "JOIN", - params: messageParamsT{ - middle: []string{ channel }, - trailing: "", - }, - }, replyT{ - command: "331", - params: messageParamsT{ - middle: []string{ u, channel }, - trailing: "No topic is set", - }, - }, replyT{ - command: "353", - params: messageParamsT{ - middle: []string{ u, "=", channel }, - trailing: u + " virtualuser", - }, - }, replyT{ - command: "366", - params: messageParamsT{ - middle: []string{ u, channel }, - trailing: "End of NAMES list", - }, - } }, nil - - - member, err := papod.queries.addMember( - *connection.user, - networkT{}, - newMemberT{}, - ) - if err != nil { - // FIXME: not allowed per RFC 1459 - return []replyT{ replyErrFileError }, nil - } - event := joinEvent(member) - - papod.metrics.nicksInChannel.Inc() - - go broadcastEvent(event, papod.receivers.get) +) ([]replyT, bool, error) { + // FIXME: add to database + channels := strings.FieldsFunc(msg.params[0], splitCommas) + papod.state.subscribe(connection.user.username, channels) - reply := asReply(event) - return []replyT{ reply }, nil + return []replyT{ + _RPL_NOTOPIC (connection, msg), + _RPL_NAMREPLY (connection, msg), + _RPL_ENDOFNAMES(connection, msg), + }, false, nil } +const minMODE = 1 func handleMODE( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - u := connection.user.username - channel := msg.params.middle[0] - return []replyT{ replyT{ - command: "324", - params: messageParamsT{ - middle: []string{ u, channel, "+Cnst" }, - trailing: "", - }, - } }, nil +) ([]replyT, bool, error) { + return []replyT{ + _RPL_CHANNELMODEIS(connection, msg), + }, false, nil } +const minWHOIS = 1 func handleWHOIS( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - u := connection.user.username - user := msg.params.middle[0] - return []replyT{ replyT{ - command: "311", - params: messageParamsT{ - middle: []string{ u, user, user, "samehost", "*" }, - trailing: "my real name is: " + user, - }, - }, replyT{ - command: "312", - params: messageParamsT{ - middle: []string{ u, user, "stillsamehost" }, - trailing: "some server info", - }, - }, replyT{ - command: "319", - params: messageParamsT{ - middle: []string{ u, user }, - trailing: "#default", - }, - }, replyT{ - command: "318", - params: messageParamsT{ - middle: []string{ u, user }, - trailing: "End of WHOIS list", - }, - } }, nil +) ([]replyT, bool, error) { + return []replyT{ + _RPL_WHOISUSER (connection, msg), + _RPL_WHOISSERVER (connection, msg), + _RPL_WHOISCHANNELS(connection, msg), + _RPL_ENDOFWHOIS (connection, msg), + }, false, nil } +const minAWAY = 0 func handleAWAY( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - u := connection.user.username - - if msg.params.trailing == "" { - return []replyT{ replyT{ - command: "305", - params: messageParamsT{ - middle: []string{ u }, - trailing: "You are no longer marked as away", - }, - } }, nil - } else { - return []replyT{ replyT{ - command: "306", - params: messageParamsT{ - middle: []string{ u }, - trailing: "You have been marked as away", - }, - } }, nil +) ([]replyT, bool, error) { + replyFn := _RPL_NOWAWAY + + if len(msg.params) == 0 { + replyFn = _RPL_UNAWAY } + + return []replyT{ + replyFn(connection, msg), + }, false, nil } +const minPING = 0 func handlePING( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - return []replyT{ { - command: "PONG", - params: messageParamsT{ - middle: []string{}, - trailing: msg.params.middle[0], - }, - } }, nil +) ([]replyT, bool, error) { + return []replyT{ + _PONG(connection, msg), + }, false, nil } +const minQUIT = 0 func handleQUIT( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - connection.conn.Close() - return []replyT{}, nil +) ([]replyT, bool, error) { + return []replyT{}, true, nil } +const minCAP = 1 func handleCAP( papod papodT, connection *connectionT, msg messageT, -) ([]replyT, error) { - if msg.params.middle[0] == "END" { - return nil, nil +) ([]replyT, bool, error) { + if msg.params[0] == "END" { + return nil, false, nil } - return []replyT{ replyT{ - command: "CAP", - params: messageParamsT{ - middle: []string { "*", "LS" }, - trailing: "", - }, - } }, nil + return []replyT{ + _CAP(connection, msg), + }, false, nil } + func authRequired( - fn func(papodT, *connectionT, messageT) ([]replyT, error), -) func(papodT, *connectionT, messageT) ([]replyT, error) { + fn func( + papodT, + *connectionT, + messageT, + ) ([]replyT, bool, error), +) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { return func( papod papodT, connection *connectionT, - message messageT, - ) ([]replyT, error) { + msg messageT, + ) ([]replyT, bool, error) { if connection.user == nil { - return []replyT{ replyErrNotRegistered }, nil + return []replyT{ + _ERR_NOTREGISTERED(connection, msg), + }, false, nil } - - return fn(papod, connection, message) + + return fn(papod, connection, msg) + } +} + +func minArgs( + count int, + fn func( + papodT, + *connectionT, + messageT, + ) ([]replyT, bool, error), +) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { + return func( + papod papodT, + connection *connectionT, + msg messageT, + ) ([]replyT, bool, error) { + if len(msg.params) < count { + return []replyT{ + _ERR_NEEDMOREPARAMS(connection, msg), + }, false, nil + } + + return fn(papod, connection, msg) } } @@ -2751,23 +4264,45 @@ var commands = map[string]func( papodT, *connectionT, messageT, -) ([]replyT, error) { - "USER": handleUSER, - "NICK": handleNICK, - "QUIT": handleQUIT, - "CAP": handleCAP, - "AWAY": authRequired(handleAWAY), - "PRIVMSG": authRequired(handlePRIVMSG), - "PING": authRequired(handlePING), - "JOIN": authRequired(handleJOIN), - "MODE": authRequired(handleMODE), - "TOPIC": authRequired(handleTOPIC), - "WHOIS": authRequired(handleWHOIS), +) ([]replyT, bool, error) { + "USER": minArgs(minUSER, handleUSER), + "NICK": minArgs(minNICK, handleNICK), + "QUIT": minArgs(minQUIT, handleQUIT), + "CAP": minArgs(minCAP, handleCAP), + "AWAY": authRequired(minArgs(minAWAY, handleAWAY)), + "PRIVMSG": authRequired(minArgs(minPRIVMSG, handlePRIVMSG)), + "PING": authRequired(minArgs(minPING, handlePING)), + "JOIN": authRequired(minArgs(minJOIN, handleJOIN)), + "MODE": authRequired(minArgs(minMODE, handleMODE)), + "TOPIC": authRequired(minArgs(minTOPIC, handleTOPIC)), + "WHOIS": authRequired(minArgs(minWHOIS, handleWHOIS)), +} + +func handleUnknown( + papod papodT, + connection *connectionT, + msg messageT, +) ([]replyT, bool, error) { + // FIXME: user doesn't exist when unauthenticated + err := papod.queries.logMessage(userT{ }, msg) + if err != nil { + g.Warning( + "Failed to log message", fmt.Sprintf("%#v", msg), + "group-as", "db-write", + "handler-action", "log-and-ignore", + "connection", connection.uuid.String(), + "err", err, + ) + } + + return []replyT{ + _ERR_UNKNOWNCOMMAND(connection, msg), + }, false, nil } func actionFnFor( command string, -) func(papodT, *connectionT, messageT) ([]replyT, error) { +) func(papodT, *connectionT, messageT) ([]replyT, bool, error) { fn := commands[command] if fn != nil { return fn @@ -2776,52 +4311,77 @@ func actionFnFor( return handleUnknown } -func replyString(reply replyT) string { - if reply.params.trailing == "" { - return fmt.Sprintf( - "%s %s\r\n", - reply.command, - strings.Join(reply.params.middle, " "), - ) - } else { - return fmt.Sprintf( - "%s %s :%s\r\n", - reply.command, - strings.Join(reply.params.middle, " "), - reply.params.trailing, - ) +func addTrailingSeparator(strs []string) { + if len(strs) == 0 { + return } + + last := strs[len(strs) - 1] + if strings.Contains(last, " ") && last[0] != ':' { + strs[len(strs) - 1] = ":" + last + } +} + +func (r replyT) String() string { + addTrailingSeparator(r.params) + return fmt.Sprintf( + "%s %s\r\n", + r.command, + strings.Join(r.params, " "), + ) +} + +func (m messageT) logAttributes() slog.Attr { + return slog.Group( + "message", + "prefix", m.prefix, + "raw", m.raw, + "params", m.params, + ) } -func processMessage(papod papodT, connection *connectionT, rawMessage string) { +func (r replyT) logAttributes() slog.Attr { + return slog.Group( + "reply", + "command", r.command, + "params", r.params, + ) +} + +func processMessage( + papod papodT, + connection *connectionT, + rawMessage string, +) { msg, err := parseMessage(rawMessage) if err != nil { g.Info( - "Error processing message", - "process-message", - "text", rawMessage, + "Error parsing message", "parse-message-error", + slog.Group( + "message", + "text", rawMessage, + ), "err", err, ) return } - - papod.metrics.receivedMessage( - "message", fmt.Sprintf("%#v", msg), - "text", rawMessage, - ) + papod.metrics.receivedMessage(msg.logAttributes()) var replyErrors []error - replies, actionErr := actionFnFor(msg.command)(papod, connection, msg) + replies, shouldClose, actionErr := actionFnFor(msg.command)( + papod, + connection, + msg, + ) for _, reply := range replies { - text := replyString(reply) - _, err = io.WriteString(connection.conn, text) + _, err = io.WriteString(connection.conn, reply.String()) if err != nil { replyErrors = append(replyErrors, err) } + papod.metrics.sentReply( - "message", rawMessage, - "reply", fmt.Sprintf("%#v", reply), - "text", text, + msg.logAttributes(), + reply.logAttributes(), ) } @@ -2842,16 +4402,20 @@ func processMessage(papod papodT, connection *connectionT, rawMessage string) { ) } - // FIXME: Close the connection + papod.state.disconnect(connection) + return + } + + if shouldClose { + papod.state.disconnect(connection) } } func handleConnection(papod papodT, conn net.Conn) { connection := connectionT{ - conn: conn, uuid: guuid.New(), - // user: nil, // FIXME: SASL shenanigan probably goes here - user: &userT{}, + user: &userT{}, // TODO: SASL shenanigan probably goes here + conn: conn, } scanner := bufio.NewScanner(conn) scanner.Split(splitOnRawMessage) @@ -2860,10 +4424,6 @@ func handleConnection(papod papodT, conn net.Conn) { } } -func handleCommand(papod papodT, conn net.Conn) { - // FIXME -} - func daemonLoop(papod papodT) { for { conn, err := papod.listeners.daemon.Accept() @@ -2880,7 +4440,6 @@ func daemonLoop(papod papodT) { ) continue } - // FIXME: where does it get closed go handleConnection(papod, conn) } } @@ -2901,7 +4460,7 @@ func commanderLoop(papod papodT) { ) continue } - go handleCommand(papod, conn) + go handleConnection(papod, conn) } } @@ -2939,7 +4498,6 @@ func (papod papodT) Start() error { "golite", golite.Version, "guuid", guuid.Version, "papod", Version, - "this", Version, ), ) diff --git a/tests/papod.go b/tests/papod.go index 42974f4..283017a 100644 --- a/tests/papod.go +++ b/tests/papod.go @@ -19,6 +19,261 @@ import ( +type userChangeT struct{ + id int64 + timestamp time.Time + user_id int64 + attribute string + valueStr *string + valueBlob *guuid.UUID + valueBool *bool + op bool +} + +type networkChangeT struct{ + id int64 + timestamp time.Time + network_id int64 + attribute string + value string + op bool +} + +type channelChangeT struct{ + id int64 + timestamp time.Time + channel_id int64 + attribute string + value string + op bool +} + + + +func userChangesSQL(prefix string) string { + const tmpl = ` + SELECT + "%s_user_changes".id, + "%s_user_changes".timestamp, + "%s_user_changes".user_id, + "%s_user_changes".attribute, + "%s_user_changes".value_text, + "%s_user_changes".value_blob, + "%s_user_changes".value_bool, + "%s_user_changes".op + FROM "%s_user_changes" + JOIN "%s_users" ON + "%s_user_changes".user_id = "%s_users".id + WHERE "%s_users".user_uuid = ? + ORDER BY "%s_user_changes".id ASC; + ` + return fmt.Sprintf( + tmpl, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ) +} + +func makeUserChanges(db *sql.DB, prefix string) func(guuid.UUID) []userChangeT { + q := userChangesSQL(prefix) + + return func(userID guuid.UUID) []userChangeT { + userChanges := []userChangeT{} + rows, err := db.Query(q, userID[:]) + g.TErrorIf(err) + defer rows.Close() + + for rows.Next() { + userChange := userChangeT{} + var ( + timestr string + value_bytes []byte + ) + err := rows.Scan( + &userChange.id, + ×tr, + &userChange.user_id, + &userChange.attribute, + &userChange.valueStr, + &value_bytes, + &userChange.valueBool, + &userChange.op, + ) + g.TErrorIf(err) + + if value_bytes != nil { + valueBlob := guuid.UUID(value_bytes) + userChange.valueBlob = &valueBlob + } + + userChange.timestamp, err = time.Parse( + time.RFC3339Nano, + timestr, + ) + g.TErrorIf(err) + + userChanges = append(userChanges, userChange) + } + + g.TErrorIf(rows.Err()) + return userChanges + } +} + +func networkChangesSQL(prefix string) string { + const tmpl = ` + SELECT + "%s_network_changes".id, + "%s_network_changes".timestamp, + "%s_network_changes".network_id, + "%s_network_changes".attribute, + "%s_network_changes".value, + "%s_network_changes".op + FROM "%s_network_changes" + JOIN "%s_networks" ON + "%s_network_changes".network_id = "%s_networks".id + WHERE "%s_networks".uuid = ? + ORDER BY "%s_network_changes".id ASC; + ` + return fmt.Sprintf( + tmpl, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + prefix, + ) +} + +func makeNetworkChanges( + db *sql.DB, + prefix string, +) func(guuid.UUID) []networkChangeT { + q := networkChangesSQL(prefix) + + return func(networkID guuid.UUID) []networkChangeT { + networkChanges := []networkChangeT{} + rows, err := db.Query(q, networkID[:]) + g.TErrorIf(err) + defer rows.Close() + + for rows.Next() { + networkChange := networkChangeT{} + var timestr string + err := rows.Scan( + &networkChange.id, + ×tr, + &networkChange.network_id, + &networkChange.attribute, + &networkChange.value, + &networkChange.op, + ) + g.TErrorIf(err) + + networkChange.timestamp, err = time.Parse( + time.RFC3339Nano, + timestr, + ) + g.TErrorIf(err) + + networkChanges = append(networkChanges, networkChange) + } + + g.TErrorIf(rows.Err()) + return networkChanges + } +} + +func channelChangesSQL(prefix string) string { + const tmpl = ` + SELECT + id, + timestamp, + channel_id, + attribute, + value_text, + value_bool, + op + FROM "%s_channel_changes" + WHERE channel_id = ? + ORDER BY id ASC; + ` + return fmt.Sprintf(tmpl, prefix) +} + +func makeChannelChanges( + db *sql.DB, + prefix string, +) func(int64) []channelChangeT { + q := channelChangesSQL(prefix) + + return func(id int64) []channelChangeT { + channelChanges := []channelChangeT{} + rows, err := db.Query(q, id) + g.TErrorIf(err) + defer rows.Close() + + for rows.Next() { + channelChange := channelChangeT{} + var ( + timestr string + valueString sql.NullString + valueBool sql.NullBool + ) + err := rows.Scan( + &channelChange.id, + ×tr, + &channelChange.channel_id, + &channelChange.attribute, + &valueString, + &valueBool, + &channelChange.op, + ) + g.TErrorIf(err) + + if valueString.Valid { + channelChange.value = valueString.String + } else if valueBool.Valid { + if valueBool.Bool { + channelChange.value = "true" + } else { + channelChange.value = "false" + } + } + + channelChange.timestamp, err = time.Parse( + time.RFC3339Nano, + timestr, + ) + g.TErrorIf(err) + + channelChanges = append(channelChanges, channelChange) + } + + g.TErrorIf(rows.Err()) + return channelChanges + } +} + func mknstring(n int) string { buffer := make([]byte, n) _, err := io.ReadFull(rand.Reader, buffer) @@ -39,14 +294,6 @@ func test_defaultPrefix() { }) } -func test_serialized() { - // FIXME -} - -func test_execSerialized() { - // FIXME -} - func test_tryRollback() { g.TestStart("tryRollback()") @@ -143,7 +390,7 @@ func test_createUserStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -159,6 +406,8 @@ func test_createUserStmt() { db.Close, ) + userChanges := makeUserChanges(db, prefix) + g.Testing("userID's must be unique", func() { newUser := newUserT{ @@ -223,6 +472,61 @@ func test_createUserStmt() { g.TErrorIf(err2) }) + g.Testing("new user trigger inserts into *_user_changes", func() { + newUser := newUserT{ + uuid: guuid.New(), + username: mkstring(), + displayName: mkstring(), + } + + _, err := createUser(newUser) + g.TErrorIf(err) + + changes := userChanges(newUser.uuid) + g.TAssertEqual(len(changes), 3) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + changes[2].attribute, + *changes[0].valueStr, + *changes[1].valueStr, + }, + []string{ + "username", + "display_name", + "deleted", + newUser.username, + newUser.displayName, + }, + ) + g.TAssertEqual(*changes[2].valueBool, false) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[2].op, + changes[0].valueBlob == nil, + changes[0].valueBool == nil, + changes[1].valueBlob == nil, + changes[1].valueBool == nil, + changes[2].valueStr == nil, + changes[2].valueBlob == nil, + }, + []bool{ + true, + true, + true, + true, + true, + true, + true, + true, + true, + }, + ) + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( createUserClose(), @@ -240,7 +544,7 @@ func test_userByUUIDStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -326,7 +630,7 @@ func test_updateUserStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -366,10 +670,12 @@ func test_updateUserStmt() { return user } + userChanges := makeUserChanges(db, prefix) + g.Testing("a user needs to exist to be updated", func() { virtualUser := userT{ - id: 1234, + id: 1234, } g.TAssertEqual(updateUser(virtualUser), sql.ErrNoRows) @@ -452,7 +758,7 @@ func test_updateUserStmt() { user2 := user1 user2.timestamp = user2.timestamp.Add(time.Minute * 1) user2.uuid = guuid.New() - err = updateUser(user2) + err := updateUser(user2) g.TErrorIf(err) user3, err := userByUUID(user1.uuid) @@ -460,6 +766,154 @@ func test_updateUserStmt() { g.TAssertEqual(user3, user1) }) + g.Testing("no extra writes to *_user_changes when not updated", func() { + user := create() + g.TAssertEqual(len(userChanges(user.uuid)), 3) + + err := updateUser(user) + g.TErrorIf(err) + g.TAssertEqual(len(userChanges(user.uuid)), 3) + }) + + g.Testing("new username end up in *_user_changes when updated", func() { + newUser := newUserT{ + uuid: guuid.New(), + username: "first username", + displayName: "display name", + } + + user, err := createUser(newUser) + g.TErrorIf(err) + g.TAssertEqual(len(userChanges(user.uuid)), 3) + + user.username = "second username" + g.TErrorIf(updateUser(user)) + + changes := userChanges(user.uuid)[3:] + g.TAssertEqual(len(changes), 2) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + *changes[0].valueStr, + *changes[1].valueStr, + }, + []string{ + "username", + "username", + "first username", + "second username", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[0].valueBlob == nil, + changes[0].valueBool == nil, + changes[1].valueBlob == nil, + changes[1].valueBool == nil, + }, + []bool{ + false, + true, + true, + true, + true, + true, + }, + ) + }) + + g.Testing("displayName end up in *_user_changes when updated", func() { + newUser := newUserT{ + uuid: guuid.New(), + username: "username", + displayName: "first display name", + } + + user, err := createUser(newUser) + g.TErrorIf(err) + g.TAssertEqual(len(userChanges(user.uuid)), 3) + + user.displayName = "second display name" + g.TErrorIf(updateUser(user)) + changes := userChanges(user.uuid)[3:] + g.TAssertEqual(len(changes), 2) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + *changes[0].valueStr, + *changes[1].valueStr, + }, + []string{ + "display_name", + "display_name", + "first display name", + "second display name", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[0].valueBlob == nil, + changes[0].valueBool == nil, + changes[1].valueBlob == nil, + changes[1].valueBool == nil, + }, + []bool{ + false, + true, + true, + true, + true, + true, + }, + ) + }) + + g.Testing("pictureID end up in *_user_changes when updated", func() { + newUser := newUserT{ + uuid: guuid.New(), + username: "username", + displayName: "first display name", + } + pictureID := guuid.New() + + user, err := createUser(newUser) + g.TErrorIf(err) + g.TAssertEqual(len(userChanges(user.uuid)), 3) + + user.pictureID = &pictureID + g.TErrorIf(updateUser(user)) + changes := userChanges(user.uuid)[3:] + g.TAssertEqual(len(changes), 1) + g.TAssertEqual(changes[0].attribute, "picture_uuid") + g.TAssertEqual(*changes[0].valueBlob, pictureID) + g.TAssertEqual(changes[0].op, true) + + user.pictureID = nil + g.TErrorIf(updateUser(user)) + changes = userChanges(user.uuid)[4:] + g.TAssertEqual(len(changes), 1) + g.TAssertEqual(changes[0].attribute, "picture_uuid") + g.TAssertEqual(*changes[0].valueBlob, pictureID) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[0].valueStr == nil, + changes[0].valueBool == nil, + }, + []bool{ + false, + true, + true, + }, + ) + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( updateUserClose(), @@ -477,7 +931,7 @@ func test_deleteUserStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -495,8 +949,11 @@ func test_deleteUserStmt() { defer g.SomeFnError( createUserClose, deleteUserClose, + db.Close, ) + userChanges := makeUserChanges(db, prefix) + g.Testing("a user needs to exist to be deleted", func() { err := deleteUser(guuid.New()) @@ -517,6 +974,49 @@ func test_deleteUserStmt() { g.TAssertEqual(err2, sql.ErrNoRows) }) + g.Testing("deletion triggers insertion into *_user_changes", func() { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + g.TAssertEqual(len(userChanges(user.uuid)), 3) + + g.TErrorIf(deleteUser(user.uuid)) + changes := userChanges(user.uuid)[3:] + g.TAssertEqual(len(changes), 2) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + }, + []string{ "deleted", "deleted" }, + ) + g.TAssertEqual( + []bool{ + *changes[0].valueBool, + changes[0].op, + changes[0].valueStr == nil, + changes[0].valueBlob == nil, + *changes[1].valueBool, + changes[1].op, + changes[1].valueStr == nil, + changes[1].valueBlob == nil, + }, + []bool{ + false, + false, + true, + true, + true, + true, + true, + true, + }, + ) + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( deleteUserClose(), @@ -527,7 +1027,6 @@ func test_deleteUserStmt() { } func test_addNetworkStmt() { - return // FIXME g.TestStart("addNetworkStmt()") const ( @@ -535,7 +1034,7 @@ func test_addNetworkStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -547,15 +1046,22 @@ func test_addNetworkStmt() { createUser, createUserClose, createUserErr := createUserStmt(cfg) deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg) addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + members, membersClose, membersErr := membersStmt(cfg) g.TErrorIf(g.SomeError( createUserErr, deleteUserErr, addNetworkErr, + membershipErr, + membersErr, )) defer g.SomeFnError( createUserClose, deleteUserClose, addNetworkClose, + membershipClose, + membersClose, + db.Close, ) create := func() userT { @@ -569,8 +1075,25 @@ func test_addNetworkStmt() { return user } + allMembers := func(actor memberT, networkID guuid.UUID) []memberT { + rows, err := members(actor) + g.TErrorIf(err) + defer rows.Close() + + var members []memberT + err = memberEach(rows, func(member memberT) error { + members = append(members, member) + return nil + }) + g.TErrorIf(err) + + return members + } + + networkChanges := makeNetworkChanges(db, prefix) - g.Testing("a user can create a newtwork", func() { + + g.Testing("a user can create a network", func() { creator := create() newNetwork := newNetworkT{ @@ -580,13 +1103,12 @@ func test_addNetworkStmt() { type_: NetworkType_Unlisted, } - network, err := addNetwork(creator, newNetwork) + network, err := addNetwork(creator, newNetwork, guuid.New()) g.TErrorIf(err) g.TAssertEqual(network.id == 0, false) g.TAssertEqual(network.timestamp == time.Time{}, false) g.TAssertEqual(network.uuid, newNetwork.uuid) - g.TAssertEqual(network.createdBy, creator.uuid) g.TAssertEqual(network.name, "the network name") g.TAssertEqual(network.description, "the network description") g.TAssertEqual(network.type_, NetworkType_Unlisted) @@ -594,14 +1116,15 @@ func test_addNetworkStmt() { g.Testing("the creator needs to exist", func() { newNetwork := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Unlisted, } virtualUser := userT{ - uuid: guuid.New(), + id: 1234, } - _, err := addNetwork(virtualUser, newNetwork) + _, err := addNetwork(virtualUser, newNetwork, guuid.New()) g.TAssertEqual( err.(golite.Error).ExtendedCode, golite.ErrConstraintNotNull, @@ -612,12 +1135,13 @@ func test_addNetworkStmt() { creator := create() newNetwork := newNetworkT{ - uuid: guuid.New(), - name: mkstring(), + uuid: guuid.New(), + name: mkstring(), + type_: NetworkType_Unlisted, } - _, err1 := addNetwork(creator, newNetwork) - _, err2 := addNetwork(creator, newNetwork) + _, err1 := addNetwork(creator, newNetwork, guuid.New()) + _, err2 := addNetwork(creator, newNetwork, guuid.New()) g.TErrorIf(err1) g.TAssertEqual( err2.(golite.Error).ExtendedCode, @@ -629,43 +1153,130 @@ func test_addNetworkStmt() { creator := create() newNetwork1 := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Unlisted, } newNetwork2 := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Unlisted, } - network1, err1 := addNetwork(creator, newNetwork1) - network2, err2 := addNetwork(creator, newNetwork2) + _, err1 := addNetwork(creator, newNetwork1, guuid.New()) + _, err2 := addNetwork(creator, newNetwork2, guuid.New()) g.TErrorIf(err1) g.TErrorIf(err2) - - g.TAssertEqual(network1.createdBy, creator.uuid) - g.TAssertEqual(network2.createdBy, creator.uuid) }) g.Testing("a deleted user can't create a network", func() { creator := create() newNetwork1 := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Unlisted, } newNetwork2 := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Unlisted, } - _, err := addNetwork(creator, newNetwork1) + _, err := addNetwork(creator, newNetwork1, guuid.New()) g.TErrorIf(err) err = deleteUser(creator.uuid) g.TErrorIf(err) - _, err = addNetwork(creator, newNetwork2) + _, err = addNetwork(creator, newNetwork2, guuid.New()) g.TAssertEqual( err.(golite.Error).ExtendedCode, golite.ErrConstraintNotNull, ) }) + g.Testing("new network triggers inserts to the changes table", func() { + creator := create() + + newNetwork := newNetworkT{ + uuid: guuid.New(), + name: "the network name", + description: "the network description", + type_: NetworkType_Unlisted, + } + + _, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + changes := networkChanges(newNetwork.uuid) + g.TAssertEqual(len(changes), 4) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + changes[2].attribute, + changes[3].attribute, + changes[0].value, + changes[1].value, + changes[2].value, + changes[3].value, + }, + []string{ + "name", + "description", + "type", + "deleted", + "the network name", + "the network description", + "unlisted", + "0", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[2].op, + }, + []bool{ true, true, true }, + ) + }) + + g.Testing("the creator is automatically a member", func() { + creator := create() + memberID := guuid.New() + + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Unlisted, + } + + network, err := addNetwork(creator, newNetwork, memberID) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + members := allMembers(member, network.uuid) + g.TAssertEqual(len(members), 1) + g.TAssertEqual(members[0].uuid, memberID) + g.TAssertEqual(members[0].status, MemberStatus_Active) + }) + + g.Testing(`the creator has "creator" and "admin" roles`, func() { + creator := create() + memberID := guuid.New() + + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Unlisted, + } + + network, err := addNetwork(creator, newNetwork, memberID) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + g.TAssertEqual(member.roles, []string{"admin", "creator"}) + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( addNetworkClose(), @@ -676,7 +1287,6 @@ func test_addNetworkStmt() { } func test_getNetworkStmt() { - return // FIXME g.TestStart("getNetworkStmt()") const ( @@ -684,7 +1294,7 @@ func test_getNetworkStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -697,6 +1307,7 @@ func test_getNetworkStmt() { deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg) addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg) g.TErrorIf(g.SomeError( @@ -704,6 +1315,7 @@ func test_getNetworkStmt() { deleteUserErr, addNetworkErr, getNetworkErr, + membershipErr, addMemberErr, dropMemberErr, )) @@ -712,8 +1324,10 @@ func test_getNetworkStmt() { deleteUserClose, addNetworkClose, getNetworkClose, + membershipClose, addMemberClose, dropMemberClose, + db.Close, ) create := func() userT { @@ -727,22 +1341,25 @@ func test_getNetworkStmt() { return user } - add := func(user userT, type_ NetworkType) networkT { + add := func(user userT, type_ NetworkType) (networkT, memberT) { newNetwork := newNetworkT{ uuid: guuid.New(), type_: type_, } - network, err := addNetwork(user, newNetwork) + network, err := addNetwork(user, newNetwork, guuid.New()) g.TErrorIf(err) - return network + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member } g.Testing("what we get is the same that was created", func() { creator := create() - network1 := add(creator, NetworkType_Public) + network1, _ := add(creator, NetworkType_Public) network2, err := getNetwork(creator, network1.uuid) g.TErrorIf(err) @@ -754,9 +1371,9 @@ func test_getNetworkStmt() { g.TAssertEqual(err, sql.ErrNoRows) }) - g.Testing("the probing user needs to exist", func() { + g.Testing("the probing member needs to exist", func() { creator := create() - network := add(creator, NetworkType_Public) + network, _ := add(creator, NetworkType_Public) virtualUser := userT{ id: 1234, @@ -772,7 +1389,7 @@ func test_getNetworkStmt() { g.Testing("the probing user can see any public network", func() { creator := create() user := create() - network := add(creator, NetworkType_Public) + network, _ := add(creator, NetworkType_Public) network1, err1 := getNetwork(creator, network.uuid) network2, err2 := getNetwork(user, network.uuid) @@ -785,7 +1402,7 @@ func test_getNetworkStmt() { g.Testing("the probing user sees the given unlisted network", func() { creator := create() user := create() - network := add(creator, NetworkType_Unlisted) + network, _ := add(creator, NetworkType_Unlisted) network1, err1 := getNetwork(creator, network.uuid) network2, err2 := getNetwork(user, network.uuid) @@ -798,7 +1415,7 @@ func test_getNetworkStmt() { g.Testing("the probing user can't see a private network", func() { creator := create() user := create() - network := add(creator, NetworkType_Private) + network, _ := add(creator, NetworkType_Private) _, err1 := getNetwork(creator, network.uuid) _, err2 := getNetwork(user, network.uuid) @@ -808,17 +1425,19 @@ func test_getNetworkStmt() { g.Testing("the probing user must be a member to see it", func() { creator := create() - member := create() - network := add(creator, NetworkType_Private) + user := create() + network, member := add(creator, NetworkType_Private) newMember := newMemberT{ - userID: member.uuid, + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), } - _, err := addMember(creator, network, newMember) + _, err := addMember(member, newMember) g.TErrorIf(err) network1, err1 := getNetwork(creator, network.uuid) - network2, err2 := getNetwork(member, network.uuid) + network2, err2 := getNetwork(user, network.uuid) g.TErrorIf(err1) g.TErrorIf(err2) g.TAssertEqual(network1, network) @@ -827,13 +1446,15 @@ func test_getNetworkStmt() { g.Testing("we can get the network if the creator was deleted", func() { creator := create() - member := create() - network := add(creator, NetworkType_Public) + user := create() + network, member := add(creator, NetworkType_Public) newMember := newMemberT{ - userID: member.uuid, + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), } - _, err := addMember(creator, network, newMember) + _, err := addMember(member, newMember) g.TErrorIf(err) network1, err := getNetwork(creator, network.uuid) @@ -842,14 +1463,14 @@ func test_getNetworkStmt() { err = deleteUser(creator.uuid) g.TErrorIf(err) - network2, err := getNetwork(member, network.uuid) + network2, err := getNetwork(user, network.uuid) g.TErrorIf(err) g.TAssertEqual(network2, network1) }) g.Testing("a deleted creator can't get a network", func() { creator := create() - network := add(creator, NetworkType_Public) + network, _ := add(creator, NetworkType_Public) _, err := getNetwork(creator, network.uuid) g.TErrorIf(err) @@ -863,7 +1484,7 @@ func test_getNetworkStmt() { g.Testing("a deleted user can't get a public network", func() { user := create() - network := add(create(), NetworkType_Public) + network, _ := add(create(), NetworkType_Public) _, err := getNetwork(user, network.uuid) g.TErrorIf(err) @@ -876,41 +1497,51 @@ func test_getNetworkStmt() { }) g.Testing("a deleted member can't get a private network", func() { - creator := create() - member := create() - network := add(creator, NetworkType_Private) + creator := create() + user := create() + network, member := add(creator, NetworkType_Private) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } - _, err := getNetwork(member, network.uuid) + _, err := addMember(member, newMember) g.TErrorIf(err) - err = deleteUser(member.uuid) + _, err = getNetwork(user, network.uuid) g.TErrorIf(err) - _, err = getNetwork(member, network.uuid) + err = deleteUser(user.uuid) + g.TErrorIf(err) + + _, err = getNetwork(user, network.uuid) g.TAssertEqual(err, sql.ErrNoRows) }) g.Testing("a removed member can't get a private network", func() { creator := create() - member := create() - network := add(creator, NetworkType_Private) + user := create() + network, member := add(creator, NetworkType_Private) newMember := newMemberT{ - userID: member.uuid, + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), } - _, err := getNetwork(member, network.uuid) + _, err := getNetwork(user, network.uuid) g.TAssertEqual(err, sql.ErrNoRows) - _, err = addMember(creator, network, newMember) + _, err = addMember(member, newMember) g.TErrorIf(err) - _, err = getNetwork(member, network.uuid) + _, err = getNetwork(user, network.uuid) g.TErrorIf(err) - err = dropMember(creator, member.uuid) + err = dropMember(member, newMember.memberID) g.TErrorIf(err) - _, err = getNetwork(member, network.uuid) + _, err = getNetwork(user, network.uuid) g.TAssertEqual(err, sql.ErrNoRows) }) @@ -924,12 +1555,181 @@ func test_getNetworkStmt() { } func test_networkEach() { - // FIXME + g.TestStart("networkEach()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + networks, networksClose, networksErr := networksStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + networksErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + networksClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) guuid.UUID { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + _, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + return newNetwork.uuid + } + + + g.Testing("callback is not called on empty set", func() { + rows, err := networks(create()) + g.TErrorIf(err) + defer rows.Close() + + networkEach(rows, func(networkT) error { + g.Unreachable() + return nil + }) + }) + + g.Testing("the callback is called once for each entry", func() { + creator := create() + networkIDs := []guuid.UUID{ + add(creator), + add(creator), + add(creator), + } + + rows, err := networks(creator) + g.TErrorIf(err) + defer rows.Close() + + var collectedIDs[]guuid.UUID + err = networkEach(rows, func(network networkT) error { + collectedIDs = append(collectedIDs, network.uuid) + return nil + }) + g.TErrorIf(err) + + g.TAssertEqual(collectedIDs, networkIDs) + }) + + g.Testing("we halt if the timestamp is ill-formatted", func() { + creator := create() + + add(creator) + add(creator) + networkID := add(creator) + add(creator) + + const tmpl = ` + UPDATE "%s_networks" + SET timestamp = %s + WHERE uuid = ?; + ` + q1 := fmt.Sprintf(tmpl, prefix, "'01/01/1970'") + _, err := db.Exec(q1, networkID[:]) + g.TErrorIf(err) + + rows, err := networks(creator) + g.TErrorIf(err) + defer rows.Close() + + n := 0 + err = networkEach(rows, func(networkT) error { + n++ + return nil + }) + + g.TAssertEqual( + err, + &time.ParseError{ + Layout: time.RFC3339Nano, + Value: "01/01/1970", + LayoutElem: "2006", + ValueElem: "01/01/1970", + Message: "", + }, + ) + g.TAssertEqual(n, 5) + + q2 := fmt.Sprintf(tmpl, prefix, g.SQLiteNow) + _, err = db.Exec(q2, networkID[:]) + g.TErrorIf(err) + }) + + g.Testing("we halt if the callback returns an error", func() { + creator := create() + myErr := errors.New("callback error early return") + + rows1, err := networks(creator) + g.TErrorIf(err) + defer rows1.Close() + + n1 := 0 + err1 := networkEach(rows1, func(networkT) error { + n1++ + if n1 == 3 { + return myErr + } + return nil + }) + + rows2, err := networks(creator) + g.TErrorIf(err) + defer rows2.Close() + + n2 := 0 + err2 := networkEach(rows2, func(networkT) error { + n2++ + return nil + }) + + g.TAssertEqual(err1, myErr) + g.TErrorIf(err2) + g.TAssertEqual(n1, 3) + g.TAssertEqual(n2, 7) + }) + + g.Testing("noop when given nil for *sql.Rows", func() { + err := networkEach(nil, func(networkT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) } func test_networksStmt() { - /* - FIXME g.TestStart("networksStmt()") const ( @@ -937,55 +1737,153 @@ func test_networksStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg) addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) networks, networksClose, networksErr := networksStmt(cfg) g.TErrorIf(g.SomeError( + createUserErr, + deleteUserErr, addNetworkErr, networksErr, )) defer g.SomeFnError( + createUserClose, + deleteUserClose, addNetworkClose, networksClose, db.Close, ) - nets := func(user userT) []networkT { - rows, err := networks(user) + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT, type_ NetworkType) networkT { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: type_, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) g.TErrorIf(err) + return network + } + + allNetworks := func(user userT) ([]networkT, error) { + rows, err := networks(user) + if err != nil { + return nil, err + } + defer rows.Close() + networkList := []networkT{} err = networkEach(rows, func(network networkT) error { networkList = append(networkList, network) return nil }) - g.TErrorIf(err) + if err != nil { + return nil, err + } - return networkList + return networkList, nil } - g.Testing("when there are no networks, we get none", func() { - // FIXME + g.Testing("when there are no networks, we get 0", func() { + networks, err := allNetworks(create()) + g.TErrorIf(err) + g.TAssertEqual(len(networks), 0) }) g.Testing("if we have only private networks, we also get none", func() { - // FIXME + creator := create() + add(creator, NetworkType_Private) + add(creator, NetworkType_Private) + add(creator, NetworkType_Private) + + networks, err := allNetworks(create()) + g.TErrorIf(err) + g.TAssertEqual(len(networks), 0) + }) + + g.Testing("when only unlisted networks, we also get none", func() { + creator := create() + add(creator, NetworkType_Unlisted) + add(creator, NetworkType_Unlisted) + add(creator, NetworkType_Unlisted) + + networks, err := allNetworks(create()) + g.TErrorIf(err) + g.TAssertEqual(len(networks), 0) }) g.Testing("we can get a list of public networks", func() { - // FIXME + creator := create() + expected := []networkT{ + add(creator, NetworkType_Public), + add(creator, NetworkType_Public), + add(creator, NetworkType_Public), + } + + networks, err := allNetworks(create()) + g.TErrorIf(err) + g.TAssertEqual(networks, expected) }) g.Testing("a member user can see their's private networks", func() { - // FIXME + creator1 := create() + creator2 := create() + add(creator1, NetworkType_Private) + add(creator1, NetworkType_Private) + add(creator1, NetworkType_Private) + add(creator2, NetworkType_Private) + add(creator2, NetworkType_Private) + add(creator2, NetworkType_Private) + + networks, err := allNetworks(creator2) + g.TErrorIf(err) + g.TAssertEqual(len(networks), 6) }) - g.Testing("unlisted networks aren't shown", func() { - // FIXME + g.Testing("a member user can see their's unlisted networks", func() { + creator1 := create() + creator2 := create() + add(creator1, NetworkType_Unlisted) + add(creator1, NetworkType_Unlisted) + add(creator1, NetworkType_Unlisted) + add(creator2, NetworkType_Unlisted) + add(creator2, NetworkType_Unlisted) + add(creator2, NetworkType_Unlisted) + + networks, err := allNetworks(creator2) + g.TErrorIf(err) + g.TAssertEqual(len(networks), 6) + }) + + g.Testing("a deleted user can't list anything", func() { + creator := create() + g.TErrorIf(deleteUser(creator.uuid)) + + _, err := allNetworks(creator) + g.TAssertEqual(err, sql.ErrNoRows) }) g.Testing("no error if closed more than once", func() { @@ -995,11 +1893,9 @@ func test_networksStmt() { networksClose(), )) }) - */ } func test_setNetworkStmt() { - return // FIXME g.TestStart("setNetworkStmt()") const ( @@ -1007,7 +1903,7 @@ func test_setNetworkStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -1020,17 +1916,35 @@ func test_setNetworkStmt() { addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg) setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addRole, addRoleClose, addRoleErr := addRoleStmt(cfg) + dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg) + editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) + dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg) g.TErrorIf(g.SomeError( createUserErr, addNetworkErr, getNetworkErr, setNetworkErr, + membershipErr, + addMemberErr, + addRoleErr, + dropRoleErr, + editMemberErr, + dropMemberErr, )) defer g.SomeFnError( createUserClose, addNetworkClose, getNetworkClose, setNetworkClose, + membershipClose, + addMemberClose, + addRoleClose, + dropRoleClose, + editMemberClose, + dropMemberClose, db.Close, ) @@ -1045,64 +1959,179 @@ func test_setNetworkStmt() { return user } - add := func(user userT) networkT { + add := func(user userT) (networkT, memberT) { newNetwork := newNetworkT{ - uuid: guuid.New(), + uuid: guuid.New(), + type_: NetworkType_Public, } + memberID := guuid.New() - network, err := addNetwork(user, newNetwork) + network, err := addNetwork(user, newNetwork, memberID) g.TErrorIf(err) - return network + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member } + networkChanges := makeNetworkChanges(db, prefix) - g.Testing("a network needs to exist to be updated", func() { + + g.Testing("creator can change the network", func() { creator := create() - virtualNetwork := networkT{ - id: 1234, - } + network1, member := add(creator) - err := setNetwork(creator, virtualNetwork) - g.TAssertEqual(err, sql.ErrNoRows) - }) + name := network1.name + "name suffix" + network1.name = name + err := setNetwork(member, network1) + g.TErrorIf(err) - g.Testing("creator can change the network", func() { - // FIXME + network2, err := getNetwork(creator, network1.uuid) + g.TErrorIf(err) + g.TAssertEqual(network2.name, name) }) - g.Testing(`"network-settings-admin" can change the network`, func() { - // FIXME + g.Testing(`"network-settings-update" can change the network`, func() { + creator := create() + admin := create() + network, creatorMember := add(creator) + newMember := newMemberT{ + userID: admin.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + adminMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = addRole( + creatorMember, + "network-settings-update", + adminMember, + ) + g.TErrorIf(err) + + name := network.name + "name suffix" + network.name = name + err = setNetwork(adminMember, network) + g.TErrorIf(err) }) - g.Testing("ex-admin creator looses ability to change it", func() { - // FIXME + g.Testing(`"admin" can change the network`, func() { + creator := create() + admin := create() + network, creatorMember := add(creator) + newMember := newMemberT{ + userID: admin.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + adminMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = addRole(creatorMember, "admin", adminMember) + g.TErrorIf(err) + + name := network.name + "name suffix" + network.name = name + err = setNetwork(adminMember, network) + g.TErrorIf(err) }) g.Testing("ex-member creator looses ability to change it", func() { - // FIXME + creator := create() + network, member := add(creator) + + err := dropMember(member, member.uuid) + g.TErrorIf(err) + + network.name = network.name + "name suffix" + err = setNetwork(member, network) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("ex-admin member looses ability to change it", func() { + creator := create() + admin := create() + network, creatorMember := add(creator) + newMember := newMemberT{ + userID: admin.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + adminMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = addRole(creatorMember, "admin", adminMember) + g.TErrorIf(err) + + name := network.name + "name suffix" + network.name = name + err = setNetwork(adminMember, network) + g.TErrorIf(err) + + err = dropRole(creatorMember, "admin", adminMember) + g.TErrorIf(err) + + err = setNetwork(adminMember, network) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) }) g.Testing("unauthorized users can't change the network", func() { creator := create() - member := create() - network := add(creator) + user := create() + network, creatorMember := add(creator) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + userMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) network.name = "member can't set the name" - err := setNetwork(member, network) - g.TAssertEqual(err, "403") + err = setNetwork(userMember, network) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("non members can't change the network", func() { + creator := create() + network, member := add(creator) + _, otherMember := add(creator) + + network.name = "non member can't set the name xablauzinho" + err1 := setNetwork(otherMember, network) + err2 := setNetwork(member, network) + g.TAssertEqual( + err1.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + g.TErrorIf(err2) }) g.Testing("after setting, getting gives us the newer data", func() { - creator := create() - network1 := add(creator) + creator := create() + network1, member := add(creator) network2 := network1 network2.name = "first network name" network2.description = "first network description" network2.type_ = NetworkType_Private - err := setNetwork(creator, network2) + err := setNetwork(member, network2) g.TErrorIf(err) network3, err := getNetwork(creator, network1.uuid) @@ -1112,19 +2141,136 @@ func test_setNetworkStmt() { g.Testing("the uuid, timestamp or creator never changes", func() { creator := create() - network1 := add(creator) + network1, member := add(creator) network2 := network1 network2.uuid = guuid.New() network2.timestamp = time.Time{} - network2.createdBy = guuid.New() - err := setNetwork(creator, network2) + err := setNetwork(member, network2) g.TErrorIf(err) network3, err := getNetwork(creator, network1.uuid) g.TErrorIf(err) g.TAssertEqual(network3, network1) + g.TAssertEqual(reflect.DeepEqual(network3, network2), false) + }) + + g.Testing("inactive member can't set the network", func() { + network, member := add(create()) + + member.status = "inactive" + err := editMember(member, member) + g.TErrorIf(err) + + err = setNetwork(member, network) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("removed member can't set the network", func() { + network, member := add(create()) + + member.status = "removed" + err := editMember(member, member) + g.TErrorIf(err) + + err = setNetwork(member, network) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("no extra writes to changes table when no updates", func() { + network, member := add(create()) + + lenBefore := len(networkChanges(network.uuid)) + + err := setNetwork(member, network) + g.TErrorIf(err) + + lenAfter := len(networkChanges(network.uuid)) + + g.TAssertEqual(lenBefore, lenAfter) + }) + + g.Testing("updates do cause writes to changes table", func() { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + name: "first name", + description: "first description", + type_: NetworkType_Public, + } + memberID := guuid.New() + + network, err := addNetwork(creator, newNetwork, memberID) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + lenBefore := len(networkChanges(network.uuid)) + + network.name = "second name" + network.description = "second description" + network.type_ = NetworkType_Unlisted + err = setNetwork(member, network) + g.TErrorIf(err) + + changes := networkChanges(network.uuid)[lenBefore:] + g.TAssertEqual(len(changes), 6) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + changes[2].attribute, + changes[3].attribute, + changes[4].attribute, + changes[5].attribute, + changes[0].value, + changes[1].value, + changes[2].value, + changes[3].value, + changes[4].value, + changes[5].value, + }, + []string{ + "type", + "type", + "description", + "description", + "name", + "name", + "public", + "unlisted", + "first description", + "second description", + "first name", + "second name", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[2].op, + changes[3].op, + changes[4].op, + changes[5].op, + }, + []bool{ + false, + true, + false, + true, + false, + true, + }, + ) }) g.Testing("no error if closed more than once", func() { @@ -1137,40 +2283,693 @@ func test_setNetworkStmt() { } func test_nipNetworkStmt() { - // FIXME + g.TestStart("nipNetworkStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg) + networks, networksClose, networksErr := networksStmt(cfg) + setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg) + nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addRole, addRoleClose, addRoleErr := addRoleStmt(cfg) + editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + getNetworkErr, + networksErr, + setNetworkErr, + nipNetworkErr, + membershipErr, + addMemberErr, + addRoleErr, + editMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + getNetworkClose, + networksClose, + setNetworkClose, + nipNetworkClose, + membershipClose, + addMemberClose, + addRoleClose, + editMemberClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) (networkT, memberT) { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member + } + + allNetworks := func(user userT) []networkT { + rows, err := networks(user) + g.TErrorIf(err) + defer rows.Close() + + networkList := []networkT{} + err = networkEach(rows, func(network networkT) error { + networkList = append(networkList, network) + return nil + }) + g.TErrorIf(err) + + return networkList + } + + networkChanges := makeNetworkChanges(db, prefix) + + + g.Testing("can't `get` a deleted network", func() { + creator := create() + network, member := add(creator) + err := nipNetwork(member) + g.TErrorIf(err) + + _, err = getNetwork(creator, network.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("after deletion it vanishes from `networks()`", func() { + creator := create() + _, member := add(creator) + + g.TAssertEqual(len(allNetworks(creator)), 1) + + err := nipNetwork(member) + g.TErrorIf(err) + + g.TAssertEqual(len(allNetworks(creator)), 0) + }) + + g.Testing("can't `set` a deleted network", func() { + network, member := add(create()) + err := nipNetwork(member) + g.TErrorIf(err) + + err = setNetwork(member, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't delete a network more than once", func() { + _, member := add(create()) + + err1 := nipNetwork(member) + err2 := nipNetwork(member) + g.TErrorIf(err1) + g.TAssertEqual(err2, sql.ErrNoRows) + }) + + g.Testing("can't get membership of a delete network", func() { + creator := create() + network, member := add(creator) + + _, err := membership(creator, network) + g.TErrorIf(err) + + err = nipNetwork(member) + g.TErrorIf(err) + + _, err = membership(creator, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("an admin can delete a network", func() { + admin := create() + _, creatorMember := add(create()) + newMember := newMemberT{ + userID: admin.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + adminMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = addRole(creatorMember, "admin", adminMember) + g.TErrorIf(err) + + err = nipNetwork(adminMember) + g.TErrorIf(err) + }) + + g.Testing("a member can't delete", func() { + user := create() + _, creatorMember := add(create()) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + userMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = nipNetwork(userMember) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("an inactive admin member also can't", func() { + user := create() + _, member := add(create()) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member.status = "inactive" + err := editMember(member, member) + g.TErrorIf(err) + + _, err = addMember(member, newMember) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintNotNull, + ) + }) + + g.Testing("deletion triggers writes to the changes table", func() { + network, member := add(create()) + + changes1Len := len(networkChanges(network.uuid)) + + err := nipNetwork(member) + g.TErrorIf(err) + + changes := networkChanges(network.uuid) + g.TAssertEqual(len(changes), changes1Len + 2) + + changes = changes[changes1Len:] + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + changes[0].value, + changes[1].value, + }, + []string{ + "deleted", + "deleted", + "0", + "1", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + }, + []bool{ + false, + true, + }, + ) + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + nipNetworkClose(), + nipNetworkClose(), + nipNetworkClose(), + )) + }) +} + +func test_membershipStmt() { + g.TestStart("membershipStmt") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + deleteUserErr, + addNetworkErr, + nipNetworkErr, + membershipErr, + addMemberErr, + dropMemberErr, + )) + defer g.SomeFnError( + createUserClose, + deleteUserClose, + addNetworkClose, + nipNetworkClose, + membershipClose, + addMemberClose, + dropMemberClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) networkT { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Private, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + return network + } + + + g.Testing("user needs to exist", func() { + virtualUser := userT{ + id: 1234, + } + network := add(create()) + + _, err := membership(virtualUser, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("network needs to exist", func() { + virtualNetwork := networkT{ + id: 1234, + } + + _, err := membership(create(), virtualNetwork) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("the member contains all of its roles", func() { + creator := create() + network := add(creator) + + member, err := membership(creator, network) + g.TErrorIf(err) + g.TAssertEqual(member.roles, []string{ "admin", "creator" }) + }) + + g.Testing("a deleted user can't get their membership", func() { + creator := create() + network := add(creator) + + err := deleteUser(creator.uuid) + g.TErrorIf(err) + + _, err = membership(creator, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't get member from a deleted network", func() { + creator := create() + network := add(creator) + + member, err := membership(creator, network) + g.TErrorIf(err) + + err = nipNetwork(member) + g.TErrorIf(err) + + _, err = membership(creator, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("we get the same data as `addMember()`", func() { + creator := create() + user := create() + network := add(creator) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: "a username", + } + + creatorMember, err := membership(creator, network) + g.TErrorIf(err) + + member1, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + member2, err := membership(user, network) + g.TErrorIf(err) + + g.TAssertEqual(member2, member1) + }) + + g.Testing("can't get membership of ex-member", func() { + creator := create() + admin := create() + network := add(creator) + newMember := newMemberT{ + userID: admin.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + creatorMember, err := membership(creator, network) + g.TErrorIf(err) + + adminMember, err := addMember(creatorMember, newMember) + g.TErrorIf(err) + + err = dropMember(adminMember, adminMember.uuid) + g.TErrorIf(err) + + _, err = membership(admin, network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("a non-member gets an error", func() { + network := add(create()) + + _, err := membership(create(), network) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("no error if closed more than once", func() { + g.TErrorIf(g.SomeError( + membershipClose(), + membershipClose(), + membershipClose(), )) }) } func test_addMemberStmt() { - /* - FIXME - g.TestStart("addMember()") + g.TestStart("addMemberStmt()") const ( dbpath = golite.InMemory prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg) addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addRole, addRoleClose, addRoleErr := addRoleStmt(cfg) g.TErrorIf(g.SomeError( + createUserErr, + deleteUserErr, addNetworkErr, + nipNetworkErr, + membershipErr, addMemberErr, + addRoleErr, )) defer g.SomeFnError( + createUserClose, + deleteUserClose, addNetworkClose, + nipNetworkClose, + membershipClose, addMemberClose, + addRoleClose, + db.Close, ) + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) (networkT, memberT) { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member + } + + + g.Testing("the user needs to exist", func() { + _, member := add(create()) + newMember := newMemberT{ + userID: guuid.New(), + memberID: guuid.New(), + username: mkstring(), + } + + _, err := addMember(member, newMember) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintNotNull, + ) + }) + + g.Testing(`the member with role "add-member" is allowed`, func() { + user1 := create() + user2 := create() + _, member0 := add(create()) + newMember1 := newMemberT{ + userID: user1.uuid, + memberID: guuid.New(), + username: mkstring(), + } + newMember2 := newMemberT{ + userID: user2.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member1, err := addMember(member0, newMember1) + g.TErrorIf(err) + + err = addRole(member0, "add-member", member1) + g.TErrorIf(err) + + _, err = addMember(member1, newMember2) + g.TErrorIf(err) + }) + + g.Testing(`the member with role "admin" is allowed`, func() { + user1 := create() + user2 := create() + _, member0 := add(create()) + newMember1 := newMemberT{ + userID: user1.uuid, + memberID: guuid.New(), + username: mkstring(), + } + newMember2 := newMemberT{ + userID: user2.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member1, err := addMember(member0, newMember1) + g.TErrorIf(err) + + err = addRole(member0, "admin", member1) + g.TErrorIf(err) + + _, err = addMember(member1, newMember2) + g.TErrorIf(err) + }) + + g.Testing(`member without role "add-member" is forbidden`, func() { + user1 := create() + user2 := create() + _, member0 := add(create()) + newMember1 := newMemberT{ + userID: user1.uuid, + memberID: guuid.New(), + username: mkstring(), + } + newMember2 := newMemberT{ + userID: user2.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member1, err := addMember(member0, newMember1) + g.TErrorIf(err) + + _, err = addMember(member1, newMember2) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintTrigger, + ) + }) + + g.Testing("can't add the same user twice", func() { + creator := create() + user := create() + _, member := add(creator) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + _, err1 := addMember(member, newMember) + _, err2 := addMember(member, newMember) + g.TErrorIf(err1) + g.TAssertEqual( + err2.(golite.Error).ExtendedCode, + golite.ErrConstraintUnique, + ) + }) + + g.Testing("the memberID must be unique", func() { + creator := create() + user1 := create() + user2 := create() + _, member := add(creator) + memberID := guuid.New() + newMember1 := newMemberT{ + userID: user1.uuid, + memberID: memberID, + username: mkstring(), + } + newMember2 := newMemberT{ + userID: user2.uuid, + memberID: memberID, + username: mkstring(), + } + + _, err1 := addMember(member, newMember1) + _, err2 := addMember(member, newMember2) + g.TErrorIf(err1) + g.TAssertEqual( + err2.(golite.Error).ExtendedCode, + golite.ErrConstraintUnique, + ) + }) + + g.Testing("the new member can't be a deleted user", func() { + creator := create() + user := create() + _, member := add(creator) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + err := deleteUser(user.uuid) + g.TErrorIf(err) + + _, err = addMember(member, newMember) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintNotNull, + ) + }) + + g.Testing("can't add to a deleted network", func() { + creator := create() + user := create() + _, member := add(creator) + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + err := nipNetwork(member) + g.TErrorIf(err) + + _, err = addMember(member, newMember) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintNotNull, + ) + }) + + g.Testing("the same user can be a member of distinct networks", func() { + // FIXME + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( addMemberClose(), @@ -1178,39 +2977,620 @@ func test_addMemberStmt() { addMemberClose(), )) }) - */ } -func test_showMemberStmt() { +func test_addRoleStmt() { + g.TestStart("addRoleStmt") + // FIXME +} + +func test_dropRoleStmt() { + g.TestStart("dropRoleStmt") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg) + showMember, showMemberClose, showMemberErr := showMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + dropRoleErr, + showMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + dropRoleClose, + showMemberClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) (networkT, memberT) { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member + } + + + g.Testing("acting member must exist", func() { + _, member := add(create()) + virtualMember := memberT{} + + err := dropRole(virtualMember, "a-role", member) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("target member must also exist", func() { + _, member := add(create()) + virtualMember := memberT{} + + err := dropRole(member, "a-role", virtualMember) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("noop if member doesn't have the role", func() { + return // FIXME + _, member := add(create()) + g.TAssertEqual(member.roles, []string{ "admin", "creator" }) + + err := dropRole(member, "does-not-exist", member) + g.TErrorIf(err) + + member, err = showMember(member, member.uuid) + g.TErrorIf(err) + g.TAssertEqual(member.roles, []string{ "admin", "creator" }) + }) + + g.Testing("role is removed when exists", func() { + _, member := add(create()) + g.TAssertEqual(member.roles, []string{ "admin", "creator" }) + + err := dropRole(member, "admin", member) + g.TErrorIf(err) + + member, err = showMember(member, member.uuid) + g.TErrorIf(err) + g.TAssertEqual(member.roles, []string{ "creator" }) + }) + + g.Testing("member can remove all roles from themselves", func() { + // FIXME + }) + + g.Testing(`member with "role-write" can drop others roles`, func() { + // FIXME + }) + + g.Testing(`member without "role-write" can't`, func() { + // FIXME + }) + + g.Testing("does not affect other members from other networks", func() { + // FIXME + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + dropRoleClose(), + dropRoleClose(), + dropRoleClose(), + )) + }) +} + +func test_showMemberStmt() { + g.TestStart("showMemberStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + showMember, showMemberClose, showMemberErr := showMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + showMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + showMemberClose, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func(user userT) (networkT, memberT) { + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return network, member + } + + + g.Testing("target member must exist", func() { + _, member := add(create()) + _, err := showMember(member, guuid.New()) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("acting member must exist", func() { + virtualMember := memberT{ + id: 1234, + } + + _, member := add(create()) + _, err := showMember(virtualMember, member.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("target user must belong to the same network", func() { + _, member1 := add(create()) + _, member2 := add(create()) + + _, err := showMember(member1, member2.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("we get the full member", func() { + _, member1 := add(create()) + member2, err := showMember(member1, member1.uuid) + g.TErrorIf(err) + + g.TAssertEqual(member2, member1) + }) + + g.Testing("no error if closed more than once", func() { + g.TErrorIf(g.SomeError( + showMemberClose(), + showMemberClose(), + showMemberClose(), )) }) } func test_memberEach() { - // FIXME + g.TestStart("memberEach()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + members, membersClose, membersErr := membersStmt(cfg) + editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addMemberErr, + membersErr, + editMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addMemberClose, + membersClose, + editMemberClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addM := func(actor memberT, status MemberStatus) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member, err := addMember(actor, newMember) + g.TErrorIf(err) + + member.status = status + err = editMember(actor, member) + g.TErrorIf(err) + + return member + } + + + g.Testing("callback is called once for new network", func() { + member := add() + + rows, err := members(member) + g.TErrorIf(err) + defer rows.Close() + + memberIDs := []guuid.UUID{} + err = memberEach(rows, func(member memberT) error { + memberIDs = append(memberIDs, member.uuid) + return nil + }) + + g.TAssertEqual(len(memberIDs), 1) + g.TAssertEqual(memberIDs[0], member.uuid) + }) + + g.Testing("we halt if the callback returns an error", func() { + myErr := errors.New("callback custom error") + member := add() + expectedIDs := []guuid.UUID{ + member.uuid, + addM(member, MemberStatus_Active).uuid, + addM(member, MemberStatus_Active).uuid, + addM(member, MemberStatus_Active).uuid, + addM(member, MemberStatus_Active).uuid, + addM(member, MemberStatus_Active).uuid, + } + + rows, err := members(member) + g.TErrorIf(err) + defer rows.Close() + + memberIDs := []guuid.UUID{} + err = memberEach(rows, func(member memberT) error { + if len(memberIDs) == 3 { + return myErr + } + + memberIDs = append(memberIDs, member.uuid) + return nil + }) + g.TAssertEqual(err, myErr) + g.TAssertEqual(len(memberIDs), 3) + g.TAssertEqual(memberIDs, expectedIDs[0:3]) + }) + + g.Testing("noop when given nil for *sql.Rows", func() { + err := memberEach(nil, func(memberT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) } func test_membersStmt() { - // FIXME + g.TestStart("membersStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + members, membersClose, membersErr := membersStmt(cfg) + editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + nipNetworkErr, + membershipErr, + addMemberErr, + membersErr, + editMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + nipNetworkClose, + membershipClose, + addMemberClose, + membersClose, + editMemberClose, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + member.roles = nil + + return member + } + + addM := func(actor memberT, status MemberStatus) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member, err := addMember(actor, newMember) + g.TErrorIf(err) + + member.status = status + err = editMember(actor, member) + g.TErrorIf(err) + + member.roles = nil + + return member + } + + allMembers := func(member memberT) []memberT { + rows, err := members(member) + g.TErrorIf(err) + defer rows.Close() + + memberList := []memberT{} + err = memberEach(rows, func(member memberT) error { + memberList = append(memberList, member) + return nil + }) + g.TErrorIf(err) + + return memberList + } + + + // FIXME: members from other networks do not show up + g.Testing("inactive and removed members aren't listed", func() { + member := add() + expected := []memberT{ + member, + addM(member, MemberStatus_Active), + addM(member, MemberStatus_Inactive), + addM(member, MemberStatus_Inactive), + addM(member, MemberStatus_Removed), + addM(member, MemberStatus_Removed), + } + + given := allMembers(member) + + g.TAssertEqual(len(given), 2) + g.TAssertEqual(given[0], expected[0]) + g.TAssertEqual(given, expected[0:2]) + }) + + g.Testing("a deleted network has 0 members", func() { + member := add() + + g.TAssertEqual(len(allMembers(member)), 1) + + err = nipNetwork(member) + g.TErrorIf(err) + + g.TAssertEqual(len(allMembers(member)), 0) + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + membersClose(), + membersClose(), + membersClose(), )) }) } func test_editMemberStmt() { + g.TestStart("editMemberStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + showMember, showMemberClose, showMemberErr := showMemberStmt(cfg) + editMember, editMemberClose, editMemberErr := editMemberStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + nipNetworkErr, + membershipErr, + showMemberErr, + editMemberErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + nipNetworkClose, + membershipClose, + showMemberClose, + editMemberClose, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + user := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return member + } + + // FIXME + // memberChanges := makeMemberChanges(db, prefix) + // FIXME + if nipNetwork != nil && showMember != nil && editMember != nil {} + if add != nil {} + + g.Testing("edit triggers writes to changes table", func() { + // FIXME + }) + g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + editMemberClose(), + editMemberClose(), + editMemberClose(), )) }) } @@ -1226,8 +3606,6 @@ func test_dropMemberStmt() { } func test_addChannelStmt() { - // FIXME - return g.TestStart("addChannelStmt()") const ( @@ -1235,7 +3613,7 @@ func test_addChannelStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -1244,65 +3622,649 @@ func test_addChannelStmt() { dbpath: dbpath, prefix: prefix, } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) - channels, channelsClose, channelsErr := channelsStmt(cfg) g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, addChannelErr, - channelsErr, )) defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, addChannelClose, - channelsClose, db.Close, ) - collect := func(workspaceID guuid.UUID) []channelT { - rows, err := channels(workspaceID) - g.TErrorIf(err) + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } - collected := []channelT{} - err = channelEach(rows, func(channel channelT) error { - collected = append(collected, channel) - return nil - }) + user, err := createUser(newUser) g.TErrorIf(err) - return collected + + return user } + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) - if true { - g.TAssertEqual(addChannel, collect) + return member } - // private channels one is not a part of doesn't show up - // channels only from the same workspace + + channelChanges := makeChannelChanges(db, prefix) + + + g.Testing("the new channel has the data it was given", func() { + member := add() + publicName := "a-name" + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: "a-label", + description: "the description", + virtual: false, + } + + channel, err := addChannel(member, newChannel) + g.TErrorIf(err) + + g.TAssertEqual(channel.id == 0, false) + g.TAssertEqual(channel.timestamp == time.Time{}, false) + g.TAssertEqual(channel.uuid, newChannel.uuid) + g.TAssertEqual(*channel.publicName, "a-name") + g.TAssertEqual(channel.label, "a-label") + g.TAssertEqual(channel.description, "the description") + g.TAssertEqual(channel.virtual, false) + }) + + g.Testing("new channel causes inserts to the changes table", func() { + member := add() + publicName := "another name" + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: "the label", + description: "the description", + virtual: false, + } + + channel, err := addChannel(member, newChannel) + g.TErrorIf(err) + + changes := channelChanges(channel.id) + g.TAssertEqual(len(changes), 4) + g.TAssertEqual( + []string{ + changes[0].attribute, + changes[1].attribute, + changes[2].attribute, + changes[3].attribute, + changes[0].value, + changes[1].value, + changes[2].value, + changes[3].value, + }, + []string{ + "public_name", + "label", + "description", + "virtual", + "another name", + "the label", + "the description", + "false", + }, + ) + g.TAssertEqual( + []bool{ + changes[0].op, + changes[1].op, + changes[2].op, + changes[3].op, + }, + []bool{ + true, + true, + true, + true, + }, + ) + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + addChannelClose(), + addChannelClose(), + addChannelClose(), )) }) } func test_channelEach() { - // FIXME + g.TestStart("channelEach()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + channels, channelsClose, channelsErr := channelsStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addChannelErr, + channelsErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addChannelClose, + channelsClose, + db.Close, + ) + + add := func() memberT { + newUser := newUserT{ + uuid: guuid.New(), + } + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return member + } + + addC := func(member memberT) channelT { + publicName := mkstring() + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: mkstring(), + virtual: true, + } + + channel, err := addChannel(member, newChannel) + g.TErrorIf(err) + + return channel + } + + + g.Testing("callback is not called on empty set", func() { + rows, err := channels(add()) + g.TErrorIf(err) + defer rows.Close() + + err = channelEach(rows, func(channelT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) + + g.Testing("the callback is called once for each entry", func() { + member := add() + expected := []channelT{ + addC(member), + addC(member), + addC(member), + } + + rows, err := channels(member) + g.TErrorIf(err) + defer rows.Close() + + channels := []channelT{} + err = channelEach(rows, func(channel channelT) error { + channels = append(channels, channel) + return nil + }) + g.TErrorIf(err) + + g.TAssertEqual(channels, expected) + }) + + g.Testing("we halt if the callback returns an error", func() { + member := add() + myErr := errors.New("callback error early return") + addC(member) + addC(member) + addC(member) + addC(member) + addC(member) + + rows1, err1 := channels(member) + rows2, err2 := channels(member) + g.TErrorIf(err1) + g.TErrorIf(err2) + defer rows1.Close() + defer rows2.Close() + + n1 := 0 + n2 := 0 + + err1 = channelEach(rows1, func(channelT) error { + n1++ + if n1 == 3 { + return myErr + } + return nil + }) + + err2 = channelEach(rows2, func(channelT) error { + n2++ + return nil + }) + + g.TAssertEqual(err1, myErr) + g.TErrorIf(err2) + g.TAssertEqual(n1, 3) + g.TAssertEqual(n2, 5) + }) + + g.Testing("noop when given nil for *sql.Rows", func() { + err := channelEach(nil, func(channelT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) } func test_channelsStmt() { - // FIXME + g.TestStart("channelsStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + channels, channelsClose, channelsErr := channelsStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addMemberErr, + addChannelErr, + channelsErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addMemberClose, + addChannelClose, + channelsClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + user := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(user, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(user, network) + g.TErrorIf(err) + + return member + } + + addM := func(member memberT) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + addedMember, err := addMember(member, newMember) + g.TErrorIf(err) + + return addedMember + } + + addC := func( + member memberT, + publicName *string, + virtual bool, + ) channelT { + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: publicName, + label: mkstring(), + description: mkstring(), + virtual: virtual, + } + + channel, err := addChannel(member, newChannel) + g.TErrorIf(err) + + return channel + } + + allChannels := func(member memberT) ([]channelT, error) { + rows, err := channels(member) + if err != nil { + return nil, err + } + defer rows.Close() + + channelList := []channelT{} + err = channelEach(rows, func(channel channelT) error { + channelList = append(channelList, channel) + return nil + }) + if err != nil { + return nil, err + } + + return channelList, nil + } + + + g.Testing("when there are no channels, we get 0", func() { + channels, err := allChannels(add()) + g.TErrorIf(err) + g.TAssertEqual(len(channels), 0) + }) + + g.Testing("when only private channels, owner gets all", func() { + member := add() + addC(member, nil, true) + addC(member, nil, true) + addC(member, nil, false) + addC(member, nil, false) + + channels, err := allChannels(member) + g.TErrorIf(err) + g.TAssertEqual(len(channels), 4) + }) + + g.Testing("when only private channels, others get none", func() { + member1 := add() + member2 := addM(member1) + addC(member1, nil, true) + addC(member1, nil, true) + addC(member1, nil, false) + addC(member1, nil, false) + + channels1, err1 := allChannels(member1) + channels2, err2 := allChannels(member2) + g.TErrorIf(err1) + g.TErrorIf(err2) + g.TAssertEqual(len(channels1), 4) + g.TAssertEqual(len(channels2), 0) + }) + + g.Testing("private channels we are a member of show up", func() { + member1 := add() + member2 := addM(member1) + name1 := "channel-name-1" + name2 := "channel-name-2" + addC(member1, nil, true) + addC(member2, nil, true) + addC(member1, nil, false) + addC(member2, nil, false) + addC(member1, &name1, true) + addC(member2, &name2, false) + + channels, err := allChannels(member1) + g.TErrorIf(err) + g.TAssertEqual(len(channels), 4) + }) + + g.Testing("we never list channels from other networks", func() { + member := add() + name1 := "a name 1" + name2 := "a name 2" + addC(member, nil, true) + addC(member, nil, false) + addC(member, &name1, true) + addC(member, &name2, false) + + channels, err := allChannels(add()) + g.TErrorIf(err) + g.TAssertEqual(len(channels), 0) + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + channelsClose(), + channelsClose(), + channelsClose(), )) }) } -func test_topicStmt() { - // FIXME +func test_setChannelStmt() { + g.TestStart("setChannelStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addMemberErr, + addChannelErr, + setChannelErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addMemberClose, + addChannelClose, + setChannelClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addM := func(actor memberT) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member, err := addMember(actor, newMember) + g.TErrorIf(err) + + return member + } + + addC := func(actor memberT, description string) channelT { + publicName := mkstring() + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: description, + virtual: false, + } + + channel, err := addChannel(actor, newChannel) + g.TErrorIf(err) + + return channel + } + + + g.Testing("acting member must exist", func() { + channel := addC(add(), "description") + virtualMember := memberT{ + id: 1234, + } + + err := setChannel(virtualMember, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("target channel must exist", func() { + virtualChannel := channelT{ + id: 1234, + } + + err := setChannel(add(), virtualChannel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("member can't set topic of other network", func() { + member := add() + otherMember := add() + channel := addC(member, "desc") + + err := setChannel(otherMember, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("actor must participate in the channel to set topic", func() { + member1 := add() + member2 := addM(member1) + channel := addC(member1, "desc") + + err := setChannel(member2, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("participant can edit", func() { + member := add() + channel := addC(member, "first description") + + channel.description = "second description" + err := setChannel(member, channel) + g.TErrorIf(err) + }) + + // we can make a private channel public + // we can make a public channel private + + g.Testing("update adds entries to *_changes table", func() { + // FIXME + }) g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + setChannelClose(), + setChannelClose(), + setChannelClose(), )) }) } @@ -1318,21 +4280,371 @@ func test_endChannelStmt() { } func test_joinStmt() { + g.TestStart("joinStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg) + join, joinClose, joinErr := joinStmt(cfg) + part, partClose, partErr := partStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addMemberErr, + addChannelErr, + setChannelErr, + joinErr, + partErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addMemberClose, + addChannelClose, + setChannelClose, + joinClose, + partClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addM := func(actor memberT) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member, err := addMember(actor, newMember) + g.TErrorIf(err) + + return member + } + + addC := func(actor memberT, publicName *string, virtual bool) channelT { + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: publicName, + label: mkstring(), + description: mkstring(), + virtual: virtual, + } + + channel, err := addChannel(actor, newChannel) + g.TErrorIf(err) + + return channel + } + + + g.Testing("acting member must exist", func() { + name := "name" + channel := addC(add(), &name, false) + virtualMember := memberT{ + id: 1234, + } + + err := join(virtualMember, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("target channel must exist", func() { + err := join(add(), guuid.New()) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't join a private channel", func() { + creator := add() + member := addM(creator) + channel := addC(creator, nil, false) + + err := join(member, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't join a virtual channel", func() { + creator := add() + member := addM(creator) + channel := addC(creator, nil, true) + + err := join(member, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't join channel in different network", func() { + name := "name" + member1 := add() + member2 := add() + channel := addC(member1, &name, false) + + err := join(member2, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("creator can't rejoin after leaving private channel", func() { + creator := add() + channel := addC(creator, nil, false) + + err := part(creator, channel) + g.TErrorIf(err) + + err = join(creator, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't join public channel one already participates", func() { + name := "name" + creator := add() + channel := addC(creator, &name, false) + + err := join(creator, channel.uuid) + g.TAssertEqual( + err.(golite.Error).ExtendedCode, + golite.ErrConstraintUnique, + ) + }) + + g.Testing("neither a private channel", func() { + creator := add() + channel := addC(creator, nil, false) + + err := join(creator, channel.uuid) + return // FIXME + g.TAssertEqual( + err, + golite.ErrConstraintUnique, + ) + }) + + g.Testing("after made public, one can join a channel", func() { + name := "name" + creator := add() + member := addM(creator) + channel := addC(creator, nil, false) + + err := join(member, channel.uuid) + g.TAssertEqual(err, sql.ErrNoRows) + + channel.publicName = &name + err = setChannel(creator, channel) + g.TErrorIf(err) + + err = join(member, channel.uuid) + g.TErrorIf(err) + }) + // FIXME + // creates "user-join" event in feed + // joining adds rows to *_changes table g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + joinClose(), + joinClose(), + joinClose(), )) }) } func test_partStmt() { + g.TestStart("partStmt()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addMember, addMemberClose, addMemberErr := addMemberStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + part, partClose, partErr := partStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addMemberErr, + addChannelErr, + partErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addMemberClose, + addChannelClose, + partClose, + db.Close, + ) + + create := func() userT{ + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addM := func(actor memberT) memberT { + user := create() + newMember := newMemberT{ + userID: user.uuid, + memberID: guuid.New(), + username: mkstring(), + } + + member, err := addMember(actor, newMember) + g.TErrorIf(err) + + return member + } + + addC := func(actor memberT, virtual bool) channelT { + publicName := "public name" + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: mkstring(), + virtual: virtual, + } + + channel, err := addChannel(actor, newChannel) + g.TErrorIf(err) + + return channel + } + + + g.Testing("acting member must exist", func() { + channel := addC(add(), false) + virtualMember := memberT{ + id: 1234, + } + + err := part(virtualMember, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("target channel must exist", func() { + virtualChannel := channelT{ + id: 1234, + } + + err := part(add(), virtualChannel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("must be a member to part", func() { + creator := add() + member := addM(creator) + channel := addC(creator, false) + + err := part(member, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can't part from a virtual channel", func() { + member := add() + channel := addC(member, true) + + err := part(member, channel) + g.TAssertEqual(err, sql.ErrNoRows) + }) + + g.Testing("can part from non-virtual channel", func() { + member := add() + channel := addC(member, false) + + err := part(member, channel) + g.TErrorIf(err) + }) + + // FIXME + // parting adds rows to *_changes table + // after parting, vanishes from member channel list g.Testing("no error if closed more than once", func() { g.TErrorIf(g.SomeError( - // FIXME + partClose(), + partClose(), + partClose(), )) }) } @@ -1352,7 +4664,6 @@ func test_namesStmt() { } func test_addEventStmt() { - return // FIXME g.TestStart("addEventStmt()") const ( @@ -1360,7 +4671,7 @@ func test_addEventStmt() { prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -1369,43 +4680,83 @@ func test_addEventStmt() { dbpath: dbpath, prefix: prefix, } - addEvent, addEventClose, addEventErr := addEventStmt(cfg) - g.TErrorIf(addEventErr) + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + addEvent, addEventClose, addEventErr := addEventStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addChannelErr, + addEventErr, + )) defer g.SomeFnError( - addEventClose, - db.Close, + createUserClose, + addNetworkClose, + membershipClose, + addChannelClose, + addEventClose, + db.Close, ) + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) - g.Testing("we can create new events", func() { - newEvent := newEventT{ - eventID: guuid.New(), - channelID: guuid.New(), - connectionID: guuid.New(), - type_: "user-message", - payload: "xablau", + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, } - _, err := addEvent(newEvent) + network, err := addNetwork(creator, newNetwork, guuid.New()) g.TErrorIf(err) - }) - g.Testing("eventID's must be unique", func() { - // FIXME - }) + member, err := membership(creator, network) + g.TErrorIf(err) - g.Testing("the database fills the generated values", func() { - const ( - type_ = "user-message" - payload = "the payload" - ) - eventID := guuid.New() + return member + } + + addC := func(actor memberT) channelT { + publicName := mkstring() + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: mkstring(), + virtual: false, + } + + channel, err := addChannel(actor, newChannel) + g.TErrorIf(err) + + return channel + } + + + g.Testing("we can create new events", func() { + creator := add() + channel := addC(creator) newEvent := newEventT{ - eventID: eventID, - channelID: guuid.New(), - connectionID: guuid.New(), - type_: type_, - payload: payload, + eventID: guuid.New(), + channelID: channel.uuid, + source: sourceT{ + uuid: creator.uuid, + type_: SourceType_Logon, + }, + type_: EventType_UserMessage, + payload: "the-payload", } event, err := addEvent(newEvent) @@ -1413,19 +4764,91 @@ func test_addEventStmt() { g.TAssertEqual(event.id == 0, false) g.TAssertEqual(event.timestamp == time.Time{}, false) - g.TAssertEqual(event.channelID == guuid.UUID{}, false) - g.TAssertEqual(event.connectionID == guuid.UUID{}, false) - g.TAssertEqual(event.uuid, eventID) - g.TAssertEqual(event.type_, type_) - g.TAssertEqual(event.payload, payload) + g.TAssertEqual(event.uuid, newEvent.eventID) + g.TAssertEqual(event.channelID, newEvent.channelID) + g.TAssertEqual(event.source, newEvent.source) + g.TAssertEqual(event.type_, EventType_UserMessage) + g.TAssertEqual(event.payload, "the-payload") + g.TAssertEqual(event.metadata == nil, true) }) - g.Testing("multiple messages can have the same connectionID", func() { - // FIXME + g.Testing("eventID's must be unique", func() { + creator := add() + channel := addC(creator) + newEvent := newEventT{ + eventID: guuid.New(), + channelID: channel.uuid, + source: sourceT{ + uuid: creator.uuid, + type_: SourceType_Logon, + }, + type_: EventType_UserMessage, + payload: "the payload", + } + + _, err1 := addEvent(newEvent) + _, err2 := addEvent(newEvent) + g.TErrorIf(err1) + g.TAssertEqual( + err2.(golite.Error).ExtendedCode, + golite.ErrConstraintUnique, + ) }) - g.Testing("messages can be dupicated: same type and payload", func() { - // FIXME + g.Testing("multiple messages can have the same source", func() { + source := sourceT{ + uuid: guuid.New(), + type_: SourceType_Logon, + } + + newEvent1 := newEventT{ + eventID: guuid.New(), + channelID: addC(add()).uuid, + source: source, + type_: EventType_UserMessage, + } + newEvent2 := newEventT{ + eventID: guuid.New(), + channelID: addC(add()).uuid, + source: source, + type_: EventType_UserMessage, + } + + _, err1 := addEvent(newEvent1) + _, err2 := addEvent(newEvent2) + g.TErrorIf(err1) + g.TErrorIf(err2) + }) + + g.Testing("messages can be duplicated: same type and payload", func() { + type_ := EventType_UserMessage + payload := "a-payload" + + newEvent1 := newEventT{ + eventID: guuid.New(), + channelID: addC(add()).uuid, + source: sourceT{ + uuid: guuid.New(), + type_: SourceType_Logon, + }, + type_: type_, + payload: payload, + } + newEvent2 := newEventT{ + eventID: guuid.New(), + channelID: addC(add()).uuid, + source: sourceT{ + uuid: guuid.New(), + type_: SourceType_Logon, + }, + type_: type_, + payload: payload, + } + + _, err1 := addEvent(newEvent1) + _, err2 := addEvent(newEvent2) + g.TErrorIf(err1) + g.TErrorIf(err2) }) g.Testing("no error if closed more than once", func() { @@ -1438,18 +4861,210 @@ func test_addEventStmt() { } func test_eventEach() { - // FIXME + g.TestStart("eventEach()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + db, err := sql.Open(golite.DriverName, dbpath) + g.TErrorIf(err) + g.TErrorIf(createTables(db, prefix)) + + cfg := dbconfigT{ + shared: db, + dbpath: dbpath, + prefix: prefix, + } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) + addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) + addEvent, addEventClose, addEventErr := addEventStmt(cfg) + allAfter, allAfterClose, allAfterErr := allAfterStmt(cfg) + g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, + addChannelErr, + addEventErr, + allAfterErr, + )) + defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, + addChannelClose, + addEventClose, + allAfterClose, + db.Close, + ) + + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addC := func(actor memberT) channelT { + publicName := mkstring() + newChannel := newChannelT{ + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: mkstring(), + virtual: false, + } + + channel, err := addChannel(actor, newChannel) + g.TErrorIf(err) + + return channel + } + + eventCount := 0 + addE := func(channelID guuid.UUID) eventT { + eventCount++ + newEvent := newEventT{ + // FIXME: missing eventID? + channelID: channelID, + source: sourceT{ + uuid: guuid.New(), + type_: SourceType_Logon, + }, + type_: EventType_UserMessage, + payload: fmt.Sprintf("event %s", eventCount), + } + + event, err := addEvent(newEvent) + g.TErrorIf(err) + + return event + } + + + g.Testing("callback is not called when there is no message", func() { + eventID := guuid.New() + member := add() + rows, err := allAfter(member, eventID) + g.TErrorIf(err) + defer rows.Close() + + err = eventEach(rows, func(eventT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) + + g.Testing("the callback is called once for each entry", func() { + return // FIXME + + eventID := guuid.New() + member := add() + channel := addC(member) + expected := []eventT{ + addE(channel.uuid), + addE(channel.uuid), + addE(channel.uuid), + } + + rows, err := allAfter(member, eventID) + g.TErrorIf(err) + defer rows.Close() + + events := []eventT{} + err = eventEach(rows, func(event eventT) error { + events = append(events, event) + return nil + }) + g.TErrorIf(err) + + g.TAssertEqual(events, expected) + }) + + g.Testing("it halts if a callback returns an error", func() { + return // FIXME + + eventID := guuid.New() + member := add() + channel := addC(member) + myErr := errors.New("callback error early return") + addE(channel.uuid) + addE(channel.uuid) + addE(channel.uuid) + addE(channel.uuid) + addE(channel.uuid) + + rows1, err1 := allAfter(member, eventID) + rows2, err2 := allAfter(member, eventID) + g.TErrorIf(err1) + g.TErrorIf(err2) + defer rows1.Close() + defer rows2.Close() + + n1 := 0 + n2 := 0 + + err1 = eventEach(rows1, func(eventT) error { + n1++ + if n1 == 3 { + return myErr + } + return nil + }) + + err2 = eventEach(rows2, func(eventT) error { + n2++ + return nil + }) + + g.TAssertEqual(n1, myErr) + g.TErrorIf(err2) + g.TAssertEqual(n1, 3) + g.TAssertEqual(n2, 5) + }) + + g.Testing("noop when given a nil for *sql.Rows", func() { + err := eventEach(nil, func(eventT) error { + g.Unreachable() + return nil + }) + g.TErrorIf(err) + }) } func test_allAfterStmt() { - g.TestStart("allAfter()") + g.TestStart("allAfterStmt()") const ( dbpath = golite.InMemory prefix = defaultPrefix ) - db, err := sql.Open(golite.DriverName, golite.InMemory) + db, err := sql.Open(golite.DriverName, dbpath) g.TErrorIf(err) g.TErrorIf(createTables(db, prefix)) @@ -1458,41 +5073,85 @@ func test_allAfterStmt() { dbpath: dbpath, prefix: prefix, } + createUser, createUserClose, createUserErr := createUserStmt(cfg) + addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg) + membership, membershipClose, membershipErr := membershipStmt(cfg) addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg) addEvent, addEventClose, addEventErr := addEventStmt(cfg) allAfter, allAfterClose, allAfterErr := allAfterStmt(cfg) g.TErrorIf(g.SomeError( + createUserErr, + addNetworkErr, + membershipErr, addChannelErr, addEventErr, allAfterErr, )) defer g.SomeFnError( + createUserClose, + addNetworkClose, + membershipClose, addChannelClose, addEventClose, allAfterClose, db.Close, ) - channel := func(publicName string) channelT { - networkID := guuid.New() + create := func() userT { + newUser := newUserT{ + uuid: guuid.New(), + } + + user, err := createUser(newUser) + g.TErrorIf(err) + + return user + } + + add := func() memberT { + creator := create() + newNetwork := newNetworkT{ + uuid: guuid.New(), + type_: NetworkType_Public, + } + + network, err := addNetwork(creator, newNetwork, guuid.New()) + g.TErrorIf(err) + + member, err := membership(creator, network) + g.TErrorIf(err) + + return member + } + + addC := func(actor memberT) channelT { + publicName := mkstring() newChannel := newChannelT{ - uuid: guuid.New(), - publicName: publicName, + uuid: guuid.New(), + publicName: &publicName, + label: mkstring(), + description: mkstring(), + virtual: false, } - channel, err := addChannel(networkID, newChannel) + channel, err := addChannel(actor, newChannel) g.TErrorIf(err) return channel } - add := func(channelID guuid.UUID, type_ string, payload string) eventT { + eventCount := 0 + addE := func(channelID guuid.UUID) eventT { + eventCount++ newEvent := newEventT{ - eventID: guuid.New(), - channelID: channelID, - connectionID: guuid.New(), - type_: type_, - payload: payload, + eventID: guuid.New(), + channelID: channelID, + source: sourceT{ + uuid: guuid.New(), + type_: SourceType_Logon, + }, + type_: EventType_UserMessage, + payload: fmt.Sprintf("event %s", eventCount), } event, err := addEvent(newEvent) @@ -1501,9 +5160,11 @@ func test_allAfterStmt() { return event } - all := func(eventID guuid.UUID) []eventT { - rows, err := allAfter(eventID) + // FIXME + allEvents := func(eventID guuid.UUID) []eventT { + rows, err := allAfter(memberT{}, eventID) g.TErrorIf(err) + defer rows.Close() events := []eventT{} err = eventEach(rows, func(event eventT) error { @@ -1517,24 +5178,65 @@ func test_allAfterStmt() { g.Testing("after joining the channel, there are no events", func() { - ch := channel("#ch") - join := add(ch.uuid, "user-join", "fulano") + return // FIXME + + eventID := guuid.New() + member := add() + channel := addC(member) expected := []eventT{ - add(ch.uuid, "user-join", "ciclano"), - add(ch.uuid, "user-join", "beltrano"), - add(ch.uuid, "user-message", "hi there"), + // FIXME: missing "user-join" event + addE(channel.uuid), + addE(channel.uuid), + addE(channel.uuid), } - given := all(join.uuid) - + given := allEvents(eventID) g.TAssertEqual(given, expected) }) g.Testing("we don't get events from other channels", func() { + return // FIXME + + eventID := guuid.New() + member := add() + channel1 := addC(member) + channel2 := addC(member) + + events := []eventT{ + addE(channel1.uuid), + addE(channel1.uuid), + addE(channel2.uuid), + addE(channel2.uuid), + addE(channel1.uuid), + addE(channel1.uuid), + } + + given := allEvents(eventID) + g.TAssertEqual(given, events[2:4]) }) g.Testing("as we change the reference point, the list changes", func() { + return // FIXME + + eventID := guuid.New() + member := add() + channel := addC(member) + + events := []eventT{ + addE(channel.uuid), + addE(channel.uuid), + addE(channel.uuid), + addE(channel.uuid), + addE(channel.uuid), + } + + g.TAssertEqual(len(allEvents(eventID)), 5) + g.TAssertEqual(len(allEvents(events[0].uuid)), 4) + g.TAssertEqual(len(allEvents(events[1].uuid)), 3) + g.TAssertEqual(len(allEvents(events[2].uuid)), 2) + g.TAssertEqual(len(allEvents(events[3].uuid)), 1) + g.TAssertEqual(len(allEvents(events[4].uuid)), 0) }) g.Testing("no error if closed more than once", func() { @@ -1544,7 +5246,6 @@ func test_allAfterStmt() { allAfterClose(), )) }) - // FIXME } func test_logMessageStmt() { @@ -1560,11 +5261,43 @@ func test_logMessageStmt() { } func test_initDB() { - // FIXME + g.TestStart("initDB()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + queries, err := initDB(dbpath, prefix) + g.TErrorIf(err) + defer queries.close() + + + g.Testing("we can perform all wrapped operations", func() { + // FIXME + }) } func test_queriesTclose() { - // FIXME + g.TestStart("queriesT.close()") + + const ( + dbpath = golite.InMemory + prefix = defaultPrefix + ) + + queries, err := initDB(dbpath, prefix) + g.TErrorIf(err) + defer queries.close() + + + g.Testing("closing more than once does not error", func() { + g.TErrorIf(g.SomeError( + queries.close(), + queries.close(), + queries.close(), + )) + }) } func test_splitOnCRLF() { @@ -1681,184 +5414,34 @@ func test_parseMessageParams() { g.Testing("we can parse the string params", func() { type tableT struct{ input string - expected messageParamsT + expected []string } table := []tableT{ - { - "", - messageParamsT{ - middle: []string { }, - trailing: "", - }, - }, - { - " ", - messageParamsT{ - middle: []string { }, - trailing: "", - }, - }, - { - " :", - messageParamsT{ - middle: []string { }, - trailing: "", - }, - }, - { - " : ", - messageParamsT{ - middle: []string { }, - trailing: " ", - }, - }, - { - ": ", - messageParamsT{ - middle: []string { ":" }, - trailing: "", - }, - }, - { - ": ", - messageParamsT{ - middle: []string { ":" }, - trailing: "", - }, - }, - { - " : ", - messageParamsT{ - middle: []string { }, - trailing: " ", - }, - }, - { - " :", - messageParamsT{ - middle: []string { }, - trailing: "", - }, - }, - { - " :", - messageParamsT{ - middle: []string { }, - trailing: "", - }, - }, - { - "a", - messageParamsT{ - middle: []string { "a" }, - trailing: "", - }, - }, - { - "ab", - messageParamsT{ - middle: []string { "ab" }, - trailing: "", - }, - }, - { - "a b", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: "", - }, - }, - { - "a b c", - messageParamsT{ - middle: []string { "a", "b", "c" }, - trailing: "", - }, - }, - { - "a b:c", - messageParamsT{ - middle: []string { "a", "b:c" }, - trailing: "", - }, - }, - { - "a b:c:", - messageParamsT{ - middle: []string { "a", "b:c:" }, - trailing: "", - }, - }, - { - "a b :c", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: "c", - }, - }, - { - "a b :c:", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: "c:", - }, - }, - { - "a b :c ", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: "c ", - }, - }, - { - "a b : c", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c", - }, - }, - { - "a b : c ", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c ", - }, - }, - { - "a b : c :", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c :", - }, - }, - { - "a b : c : ", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c : ", - }, - }, - { - "a b : c :d", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c :d", - }, - }, - { - "a b : c :d ", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c :d ", - }, - }, - { - "a b : c : d ", - messageParamsT{ - middle: []string { "a", "b" }, - trailing: " c : d ", - }, - }, + { "", []string{} }, + { " ", []string{} }, + { " :", []string{ "" } }, + { " : ", []string{ " " } }, + { ": ", []string{ ":" } }, + { ": ", []string{ ":" } }, + { " : ", []string{ " " } }, + { " :", []string{ "" } }, + { " :", []string{ "" } }, + { "a", []string{ "a" } }, + { "ab", []string{ "ab" } }, + { "a b", []string{ "a", "b" } }, + { "a b c", []string{ "a", "b", "c" } }, + { "a b:c", []string{ "a", "b:c" } }, + { "a b:c:", []string{ "a", "b:c:" } }, + { "a b :c", []string{ "a", "b", "c" } }, + { "a b :c:", []string{ "a", "b", "c:" } }, + { "a b :c ", []string{ "a", "b", "c " } }, + { "a b : c", []string{ "a", "b", " c" } }, + { "a b : c ", []string{ "a", "b", " c " } }, + { "a b : c :", []string{ "a", "b", " c :" } }, + { "a b : c : ", []string{ "a", "b", " c : " } }, + { "a b : c :d", []string{ "a", "b", " c :d" } }, + { "a b : c :d ", []string{ "a", "b", " c :d " } }, + { "a b : c : d ", []string{ "a", "b", " c : d " } }, } for _, entry := range table { @@ -1881,10 +5464,7 @@ func test_parseMessage() { messageT{ prefix: "", command: "NICK", - params: messageParamsT{ - middle: []string { "joebloe" }, - trailing: "", - }, + params: []string{ "joebloe" }, raw: "NICK joebloe ", }, }, { @@ -1892,11 +5472,11 @@ func test_parseMessage() { messageT{ prefix: "", command: "USER", - params: messageParamsT{ - middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - trailing: "Joe Bloe", + params: []string{ + "joebloe", + "0.0.0.0", + "joe", + "Joe Bloe", }, raw: "USER joebloe 0.0.0.0 joe :Joe Bloe", }, @@ -1905,11 +5485,11 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - trailing: "Joe Bloe", + params: []string{ + "joebloe", + "0.0.0.0", + "joe", + "Joe Bloe", }, raw: ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", }, @@ -1918,11 +5498,11 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - trailing: " Joe Bloe ", + params: []string{ + "joebloe", + "0.0.0.0", + "joe", + " Joe Bloe ", }, raw: ":pre USER joebloe 0.0.0.0 " + "joe : Joe Bloe ", @@ -1932,11 +5512,11 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { - "jbloe:", "0:0:0:1", "joe::a:", - }, - trailing: " Joe Bloe ", + params: []string{ + "jbloe:", + "0:0:0:1", + "joe::a:", + " Joe Bloe ", }, raw: ":pre USER jbloe: 0:0:0:1 " + "joe::a: : Joe Bloe ", @@ -1946,10 +5526,7 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { }, - trailing: "Joe Bloe", - }, + params: []string{ "Joe Bloe" }, raw: ":pre USER :Joe Bloe", }, }, { @@ -1957,10 +5534,7 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { }, - trailing: " Joe Bloe", - }, + params: []string{ " Joe Bloe" }, raw: ":pre USER : Joe Bloe", }, }, { @@ -1968,10 +5542,7 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { }, - trailing: " Joe Bloe", - }, + params: []string{ " Joe Bloe" }, raw: ":pre USER : Joe Bloe", }, }, { @@ -1979,10 +5550,7 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { }, - trailing: " ", - }, + params: []string{ " " }, raw: ":pre USER : ", }, }, { @@ -1990,10 +5558,7 @@ func test_parseMessage() { messageT{ prefix: "pre", command: "USER", - params: messageParamsT{ - middle: []string { }, - trailing: "", - }, + params: []string{}, raw: ":pre USER :", }, }} @@ -2034,10 +5599,72 @@ func test_parseMessage() { }) } +func test_addTrailingSeparator() { + g.TestStart("addTrailingSeparator()") + + g.Testing("noop on empty slice", func() { + input := []string{} + addTrailingSeparator(input) + g.TAssertEqual(input, []string{}) + }) + + g.Testing("noop if last doesn't have a space", func() { + type tableT struct{ + input []string + expected []string + } + + entries := []tableT{ + { []string{ "" }, []string{ "" } }, + { []string{ "", "" }, []string{ "", "" } }, + { []string{ "a", "b" }, []string{ "a", "b" } }, + { + []string{ "a", "b", "c-d" }, + []string{ "a", "b", "c-d" }, + }, + { + []string{ "a ", "b", "cd" }, + []string{ "a ", "b", "cd" }, + }, + } + + for i, entry := range entries { + addTrailingSeparator(entry.input) + g.TAssertEqual(entry.input, entries[i].expected) + } + }) + + g.Testing("add ':' to the last otherwise", func() { + type tableT struct{ + input []string + expected []string + } + + entries := []tableT{ + { []string{ " " }, []string{ ": " } }, + { []string{ "a " }, []string{ ":a " } }, + { []string{ " a" }, []string{ ": a" } }, + { []string{ "a ", " b" }, []string{ "a ", ": b" } }, + { []string{ "a", " b" }, []string{ "a", ": b" } }, + { + []string{ "a", "b", "c d" }, + []string{ "a", "b", ":c d" }, + }, + } + + for i, entry := range entries { + addTrailingSeparator(entry.input) + addTrailingSeparator(entry.input) + g.TAssertEqual(entry.input, entries[i].expected) + } + }) +} + func dumpQueries() { queries := []struct{name string; fn func(string) queryT}{ { "createTables", createTablesSQL }, + { "memberRoles", memberRolesSQL }, { "createUser", createUserSQL }, { "userByUUID", userByUUIDSQL }, { "updateUser", updateUserSQL }, @@ -2047,14 +5674,17 @@ func dumpQueries() { { "networks", networksSQL }, { "setNetwork", setNetworkSQL }, { "nipNetwork", nipNetworkSQL }, + { "membership", membershipSQL }, { "addMember", addMemberSQL }, + { "addRole", addRoleSQL }, + { "dropRole", dropRoleSQL }, { "showMember", showMemberSQL }, { "members", membersSQL }, { "editMember", editMemberSQL }, { "dropMember", dropMemberSQL }, { "addChannel", addChannelSQL }, { "channels", channelsSQL }, - { "topic", topicSQL }, + { "setChannel", setChannelSQL }, { "endChannel", endChannelSQL }, { "join", joinSQL }, { "part", partSQL }, @@ -2081,8 +5711,6 @@ func MainTest() { g.Init() test_defaultPrefix() - test_serialized() - test_execSerialized() test_tryRollback() test_inTx() test_createTables() @@ -2096,7 +5724,10 @@ func MainTest() { test_networksStmt() test_setNetworkStmt() test_nipNetworkStmt() + test_membershipStmt() test_addMemberStmt() + test_addRoleStmt() + test_dropRoleStmt() test_showMemberStmt() test_memberEach() test_membersStmt() @@ -2105,7 +5736,7 @@ func MainTest() { test_addChannelStmt() test_channelEach() test_channelsStmt() - test_topicStmt() + test_setChannelStmt() test_endChannelStmt() test_joinStmt() test_partStmt() @@ -2121,4 +5752,5 @@ func MainTest() { test_splitOnRawMessage() test_parseMessageParams() test_parseMessage() + test_addTrailingSeparator() } diff --git a/tests/queries.sql b/tests/queries.sql index c996f02..992c8d2 100644 --- a/tests/queries.sql +++ b/tests/queries.sql @@ -1,134 +1,271 @@ -- createTables.sql: -- write: - -- FIXME: unconfirmed premise: statements within a trigger are - -- part of the transaction that caused it, and so are - -- atomic. + -- TODO: unconfirmed premise: statements within a trigger are + -- part of the transaction that caused it, and so are + -- atomic. -- See also: -- https://stackoverflow.com/questions/77441888/ -- https://stackoverflow.com/questions/30511116/ CREATE TABLE IF NOT EXISTS "papod_users" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), -- provided by cracha - uuid BLOB NOT NULL UNIQUE, + user_uuid BLOB NOT NULL UNIQUE, username TEXT NOT NULL, display_name TEXT NOT NULL, picture_uuid BLOB UNIQUE, deleted INT NOT NULL CHECK(deleted IN (0, 1)) ) STRICT; --- CREATE TABLE IF NOT EXISTS "papod_user_changes" ( --- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, --- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), --- user_id INTEGER NOT NULL REFERENCES "papod_users"(id), --- attribute TEXT NOT NULL CHECK( --- attribute IN ( --- 'username', --- 'display_name', --- 'picture_uuid', --- 'deleted' --- ) --- ), --- value TEXT NOT NULL, --- op INT NOT NULL CHECK(op IN (0, 1)) --- ) STRICT; --- CREATE TRIGGER IF NOT EXISTS "papod_user_creation" --- AFTER INSERT ON "papod_users" --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'username', NEW.username, true), --- (NEW.id, 'display_name', NEW.display_name, true), --- (NEW.id, 'deleted', NEW.deleted, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "papod_user_creation_picture_uuid" --- AFTER INSERT ON "papod_users" --- WHEN NEW.picture_uuid != NULL --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'picture_uuid', NEW.picture_uuid, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "papod_user_update_username" --- AFTER UPDATE ON "papod_users" --- WHEN OLD.username != NEW.username --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'username', OLD.username, false), --- (NEW.id, 'username', NEW.username, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "papod_user_update_display_name" --- AFTER UPDATE ON "papod_users" --- WHEN OLD.display_name != NEW.display_name --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'display_name', OLD.display_name, false), --- (NEW.id, 'display_name', NEW.display_name, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "papod_user_update_picture_uuid" --- AFTER UPDATE ON "papod_users" --- WHEN OLD.picture_uuid != NEW.picture_uuid --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'picture_uuid', OLD.picture_uuid, false), --- (NEW.id, 'picture_uuid', NEW.picture_uuid, true) --- ; --- END; --- CREATE TRIGGER IF NOT EXISTS "papod_user_update_deleted" --- AFTER UPDATE ON "papod_users" --- WHEN OLD.deleted != NEW.deleted --- BEGIN --- INSERT INTO "papod_user_changes" ( --- user_id, attribute, value, op --- ) VALUES --- (NEW.id, 'deleted', OLD.deleted, false), --- (NEW.id, 'deleted', NEW.deleted, true) --- ; --- END; + CREATE TABLE IF NOT EXISTS "papod_user_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + user_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'username', + 'display_name', + 'picture_uuid', + 'deleted' + ) + ), + value_text TEXT, + value_blob BLOB, + value_bool INT CHECK(value_bool IN (0, 1)), + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; + CREATE TRIGGER IF NOT EXISTS "papod_user_new" + AFTER INSERT ON "papod_users" + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', NEW.username, true), + (NEW.id, 'display_name', NEW.display_name, true) + ; + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_new_picture_uuid" + AFTER INSERT ON "papod_users" + WHEN NEW.picture_uuid IS NOT NULL + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_update_username" + AFTER UPDATE ON "papod_users" + WHEN OLD.username != NEW.username + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', OLD.username, false), + (NEW.id, 'username', NEW.username, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_update_display_name" + AFTER UPDATE ON "papod_users" + WHEN OLD.display_name != NEW.display_name + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_text, op + ) VALUES + (NEW.id, 'display_name', OLD.display_name, false), + (NEW.id, 'display_name', NEW.display_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_add_picture_uuid" + AFTER UPDATE ON "papod_users" + WHEN ( + OLD.picture_uuid IS NULL AND + NEW.picture_uuid IS NOT NULL + ) + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_remove_picture_uuid" + AFTER UPDATE ON "papod_users" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NULL + ) + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_update_picture_uuid" + AFTER UPDATE ON "papod_users" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NOT NULL AND + OLD.picture_uuid != NEW.picture_uuid + ) + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false), + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_user_update_deleted" + AFTER UPDATE ON "papod_users" + WHEN OLD.deleted != NEW.deleted + BEGIN + INSERT INTO "papod_user_changes" ( + user_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'deleted', OLD.deleted, false), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + + CREATE TABLE IF NOT EXISTS "papod_sessions" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + -- provided by cracha + session_uuid BLOB NOT NULL UNIQUE, + user_id INTEGER NOT NULL + REFERENCES "papod_users"(id), + finished_at TEXT + ); + CREATE TABLE IF NOT EXISTS "papod_connections" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + uuid BLOB NOT NULL UNIQUE, + finished_at TEXT + ); + CREATE TABLE IF NOT EXISTS "papod_logons" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + session_id INTEGER NOT NULL + REFERENCES "papod_sessions"(id), + connection_id INTEGER NOT NULL + REFERENCES "papod_connections"(id), + UNIQUE (session_id, connection_id) + ) STRICT; CREATE TABLE IF NOT EXISTS "papod_networks" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), uuid BLOB NOT NULL UNIQUE, - creator_id INTEGER NOT NULL REFERENCES "papod_users"(id), name TEXT NOT NULL, description TEXT NOT NULL, type TEXT NOT NULL CHECK( type IN ('public', 'private', 'unlisted') - ) + ), + deleted INT NOT NULL CHECK(deleted IN (0, 1)) ) STRICT; + CREATE INDEX IF NOT EXISTS "papod_networks_type" + ON "papod_networks"(type); CREATE TABLE IF NOT EXISTS "papod_network_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), - network_id INTEGER NOT NULL - REFERENCES "papod_networks"(id), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + network_id INTEGER NOT NULL, attribute TEXT NOT NULL CHECK( attribute IN ( 'name', 'description', - 'type' + 'type', + 'deleted', + 'logon_id' -- FIXME ) ), value TEXT NOT NULL, op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "papod_network_new" + AFTER INSERT ON "papod_networks" + BEGIN + INSERT INTO "papod_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'name', NEW.name, true), + (NEW.id, 'description', NEW.description, true), + (NEW.id, 'type', NEW.type, true), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_network_update_name" + AFTER UPDATE ON "papod_networks" + WHEN OLD.name != NEW.name + BEGIN + INSERT INTO "papod_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'name', OLD.name, false), + (NEW.id, 'name', NEW.name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_network_update_description" + AFTER UPDATE ON "papod_networks" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "papod_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'description', OLD.description, false), + (NEW.id, 'description', NEW.description, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_network_update_type" + AFTER UPDATE ON "papod_networks" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "papod_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'type', OLD.type, false), + (NEW.id, 'type', NEW.type, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_network_update_deleted" + AFTER UPDATE ON "papod_networks" + WHEN OLD.deleted != NEW.deleted + BEGIN + INSERT INTO "papod_network_changes" ( + network_id, attribute, value, op + ) VALUES + (NEW.id, 'deleted', OLD.deleted, false), + (NEW.id, 'deleted', NEW.deleted, true) + ; + END; CREATE TABLE IF NOT EXISTS "papod_members" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + uuid BLOB NOT NULL UNIQUE, network_id INTEGER NOT NULL REFERENCES "papod_networks"(id), user_id INTEGER NOT NULL, @@ -144,6 +281,120 @@ UNIQUE (network_id, username, active_uniq), UNIQUE (network_id, user_id) ) STRICT; + CREATE TABLE IF NOT EXISTS "papod_member_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + member_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'username', + 'display_name', + 'picture_uuid', + 'status', + 'logon_id' -- FIXME + ) + ), + value_text TEXT, + value_blob BLOB, + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; + CREATE TRIGGER IF NOT EXISTS "papod_member_new" + AFTER INSERT ON "papod_members" + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', NEW.username, true), + (NEW.id, 'display_name', NEW.display_name, true), + (NEW.id, 'status', NEW.status, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_new_picture_uuid" + AFTER INSERT ON "papod_members" + WHEN NEW.picture_uuid IS NOT NULL + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_update_username" + AFTER UPDATE ON "papod_members" + WHEN OLD.username != NEW.username + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'username', OLD.username, false), + (NEW.id, 'username', NEW.username, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_update_display_name" + AFTER UPDATE ON "papod_members" + WHEN OLD.display_name != NEW.display_name + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'display_name', OLD.display_name, false), + (NEW.id, 'display_name', NEW.display_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_update_status" + AFTER UPDATE ON "papod_members" + WHEN OLD.status != NEW.status + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_text, op + ) VALUES + (NEW.id, 'status', OLD.status, false), + (NEW.id, 'status', NEW.status, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_add_picture_uuid" + AFTER UPDATE ON "papod_members" + WHEN ( + OLD.picture_uuid IS NULL AND + NEW.picture_uuid IS NOT NULL + ) + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_remove_picture_uuid" + AFTER UPDATE ON "papod_members" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NULL + ) + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_update_picture_uuid" + AFTER UPDATE ON "papod_members" + WHEN ( + OLD.picture_uuid IS NOT NULL AND + NEW.picture_uuid IS NOT NULL AND + OLD.picture_uuid != NEW.picture_uuid + ) + BEGIN + INSERT INTO "papod_member_changes" ( + member_id, attribute, value_blob, op + ) VALUES + (NEW.id, 'picture_uuid', OLD.picture_uuid, false), + (NEW.id, 'picture_uuid', NEW.picture_uuid, true) + ; + END; CREATE TABLE IF NOT EXISTS "papod_member_roles" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -152,40 +403,157 @@ role TEXT NOT NULL, UNIQUE (member_id, role) ) STRICT; - - -- FIXME: use a trigger - CREATE TABLE IF NOT EXISTS "papod_member_changes" ( + CREATE TABLE IF NOT EXISTS "papod_member_role_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), - member_id INTEGER NOT NULL - REFERENCES "papod_members"(id), - attribute TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + role_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'role', + 'logon_id' -- FIXME + ) + ), value TEXT NOT NULL, op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "papod_member_role_add" + AFTER INSERT ON "papod_member_roles" + BEGIN + INSERT INTO "papod_member_role_changes" ( + role_id, attribute, value, op + ) VALUES + (NEW.id, 'role', NEW.role, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_member_role_remove" + AFTER DELETE ON "papod_member_roles" + BEGIN + INSERT INTO "papod_member_role_changes" ( + role_id, attribute, value, op + ) VALUES + (OLD.id, 'role', OLD.role, false) + ; + END; CREATE TABLE IF NOT EXISTS "papod_channels" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), uuid BLOB NOT NULL UNIQUE, - network_id INTEGER -- FIXME NOT NULL + network_id INTEGER NOT NULL REFERENCES "papod_networks"(id), - public_name TEXT UNIQUE, + public_name TEXT, label TEXT NOT NULL, description TEXT NOT NULL, - virtual INT NOT NULL CHECK(virtual IN (0, 1)) + virtual INT NOT NULL CHECK(virtual IN (0, 1)), + UNIQUE (network_id, public_name) ) STRICT; - - -- FIXME: use a trigger CREATE TABLE IF NOT EXISTS "papod_channel_changes" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), - channel_id INTEGER NOT NULL - REFERENCES "papod_channels"(id), - attribute TEXT NOT NULL, - value TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + channel_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'public_name', + 'label', + 'description', + 'virtual', + 'logon_id' -- FIXME + ) + ), + value_text TEXT, + value_bool INT CHECK(value_bool IN (0, 1)), op INT NOT NULL CHECK(op IN (0, 1)) ) STRICT; + CREATE TRIGGER IF NOT EXISTS "papod_channel_new" + AFTER INSERT ON "papod_channels" + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'label', NEW.label, true), + (NEW.id, 'description', NEW.description, true) + ; + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'virtual', NEW.virtual, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_new_public_name" + AFTER INSERT ON "papod_channels" + WHEN NEW.public_name IS NOT NULL + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'public_name', NEW.public_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_update_label" + AFTER UPDATE ON "papod_channels" + WHEN OLD.label != NEW.label + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'label', OLD.label, false), + (NEW.id, 'label', NEW.label, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_update_description" + AFTER UPDATE ON "papod_channels" + WHEN OLD.description != NEW.description + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'description', OLD.description, false), + (NEW.id, 'description', NEW.description, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_update_virtual" + AFTER UPDATE ON "papod_channels" + WHEN OLD.virtual != NEW.virtual + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_bool, op + ) VALUES + (NEW.id, 'virtual', OLD.virtual, false), + (NEW.id, 'virtual', NEW.virtual, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_add_public_name" + AFTER UPDATE ON "papod_channels" + WHEN ( + OLD.public_name IS NULL AND + NEW.public_name IS NOT NULL + ) + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (NEW.id, 'public_name', NEW.public_name, true) + ; + END; + CREATE TRIGGER IF NOT EXISTS "papod_channel_remove_public_name" + AFTER UPDATE ON "papod_channels" + WHEN ( + OLD.public_name IS NOT NULL AND + NEW.public_name IS NULL + ) + BEGIN + INSERT INTO "papod_channel_changes" ( + channel_id, attribute, value_text, op + ) VALUES + (OLD.id, 'public_name', OLD.public_name, false) + ; + END; CREATE TABLE IF NOT EXISTS "papod_participants" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -195,37 +563,65 @@ REFERENCES "papod_members"(id), UNIQUE (channel_id, member_id) ) STRICT; + CREATE TABLE IF NOT EXISTS "papod_participant_changes" ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), + participant_id INTEGER NOT NULL, + attribute TEXT NOT NULL CHECK( + attribute IN ( + 'connection_id' + ) + ), + value TEXT NOT NULL, + op INT NOT NULL CHECK(op IN (0, 1)) + ) STRICT; - -- FIXME: create database table for connections? - -- A user can have multiple sessions (different browsers, - -- mobile, etc.), and each session has multiple connections, as - -- the user connects and disconnections using the same session - -- id, all while it is valid. - -- FIXME: can a connection have multiple sessions? A long-lived - -- connection that spans multiple sessions would fit into this. CREATE TABLE IF NOT EXISTS "papod_channel_events" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')), + timestamp TEXT NOT NULL DEFAULT ( + strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now') + ), uuid BLOB NOT NULL UNIQUE, channel_id INTEGER NOT NULL REFERENCES "papod_channels"(id), - connection_uuid BLOB NOT NULL, -- FIXME: join + source_uuid BLOB NOT NULL, + source_type TEXT NOT NULL CHECK( + source_type IN ( + 'logon' + ) + ), + source_metadata TEXT, type TEXT NOT NULL CHECK( type IN ( 'user-join', 'user-message' ) ), - payload TEXT NOT NULL + payload TEXT NOT NULL, + metadata TEXT ) STRICT; + -- read: +-- memberRoles.sql: +-- write: + +-- read: + SELECT role FROM "papod_member_roles" + JOIN "papod_members" ON + "papod_member_roles".member_id = "papod_members".id + WHERE "papod_members".uuid = ? + ORDER BY "papod_member_roles".id; + + -- createUser.sql: -- write: INSERT INTO "papod_users" ( - uuid, username, display_name, picture_uuid, deleted + user_uuid, username, display_name, picture_uuid, deleted ) VALUES ( ?, ?, ?, NULL, false ) RETURNING id, timestamp; @@ -245,8 +641,8 @@ picture_uuid FROM "papod_users" WHERE - uuid = ? AND - deleted = false; + user_uuid = ? AND + deleted = false; -- updateUser.sql: @@ -269,8 +665,8 @@ UPDATE "papod_users" SET deleted = true WHERE - uuid = ? AND - deleted = false + user_uuid = ? AND + deleted = false RETURNING id; @@ -279,92 +675,267 @@ -- addNetwork.sql: -- write: INSERT INTO "papod_networks" ( - uuid, name, description, type, creator_id + uuid, name, description, type, deleted ) VALUES ( ?, ?, ?, ?, - ( - SELECT id FROM "papod_users" - WHERE id = ? AND deleted = false - ) - ) RETURNING id, timestamp; + false + ) RETURNING id; + WITH creator AS ( + SELECT username, display_name, picture_uuid + FROM "papod_users" + WHERE id = ? AND deleted = false + ), new_network AS ( + SELECT id FROM "papod_networks" WHERE uuid = ? + ) INSERT INTO "papod_members" ( - network_id, user_id, username, display_name, + uuid, network_id, user_id, username, display_name, picture_uuid, status, active_uniq ) VALUES ( - last_insert_rowid(), ?, - ( - SELECT username, display_name, picture_uuid - FROM "papod_users" - WHERE id = ? AND deleted = false - ), + (SELECT id FROM new_network), + ?, + (SELECT username FROM creator), + (SELECT display_name FROM creator), + (SELECT picture_uuid FROM creator), 'active', 'active' - ) RETURNING id, timestamp; + ) RETURNING id; + + WITH new_member AS ( + SELECT id FROM "papod_members" WHERE uuid = ? + ) + INSERT INTO "papod_member_roles" (member_id, role) + VALUES ( + (SELECT id FROM new_member), + 'admin' + ), + ( + (SELECT id FROM new_member), + 'creator' + ) + RETURNING id; -- read: + SELECT id, timestamp FROM "papod_networks" + WHERE uuid = ? AND deleted = false; + -- getNetwork.sql: -- write: -- read: + WITH probing_user AS ( + SELECT id FROM "papod_users" + WHERE id = ? AND deleted = false + ), target_network AS ( + SELECT id FROM "papod_networks" + WHERE uuid = ? AND deleted = false + ) SELECT - "papod_networks".id, - "papod_networks".timestamp, - "papod_users".uuid, - "papod_networks".name, - "papod_networks".description, - "papod_networks".type + id, + timestamp, + name, + description, + type FROM "papod_networks" - JOIN "papod_users" ON - "papod_users".id = "papod_networks".creator_id WHERE - "papod_networks".uuid = $networkUUID AND - $userID IN ( - SELECT id FROM "papod_users" - WHERE id = $userID AND deleted = false - ) AND + uuid = ? AND + deleted = false AND + ? IN probing_user AND ( - "papod_networks".type IN ('public', 'unlisted') OR - $userID IN ( + type IN ('public', 'unlisted') OR + ? IN ( SELECT user_id FROM "papod_members" WHERE - user_id = $userID AND - network_id = "papod_networks".id + user_id = ? AND + network_id IN target_network AND + status != 'removed' ) ); - + %!(EXTRA string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod) -- networks.sql: -- write: -- read: - -- FIXME papod + WITH current_user AS ( + SELECT id, deleted FROM "papod_users" WHERE id = ? + ) + SELECT + "papod_networks".id, + "papod_networks".timestamp, + "papod_networks".uuid, + "papod_networks".name, + "papod_networks".description, + "papod_networks".type, + (SELECT deleted FROM current_user) + FROM "papod_networks" + JOIN "papod_members" ON + "papod_networks".id = "papod_members".network_id + WHERE ( + "papod_networks".type = 'public' OR + "papod_networks".id IN ( + SELECT network_id FROM "papod_members" + WHERE user_id IN (SELECT id FROM current_user) + ) + ) AND "papod_networks".deleted = false + ORDER BY "papod_networks".id; -- setNetwork.sql: -- write: - -- FIXME papod + UPDATE "papod_networks" + SET + name = ?, + description = ?, + type = ? + WHERE id = ? AND deleted = false + RETURNING ( + SELECT CASE WHEN EXISTS ( + SELECT role from "papod_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin', + 'network-settings-update' + ) AND ? IN ( + SELECT network_id + FROM "papod_members" + WHERE + id = ? AND + status = 'active' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to update network data' + ) END + ); + -- read: -- nipNetwork.sql: -- write: - -- FIXME papod + WITH target_network AS ( + SELECT network_id AS id + FROM "papod_members" + WHERE + id = ? AND + status = 'active' + ) + UPDATE "papod_networks" + SET deleted = true + WHERE id IN target_network AND deleted = false + RETURNING ( + SELECT CASE WHEN EXISTS ( + SELECT role FROM "papod_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to delete network' + ) END + ); -- read: +-- membership.sql: +-- write: + +-- read: + SELECT + "papod_members".id, + "papod_members".timestamp, + "papod_members".uuid, + "papod_members".username, + "papod_members".display_name, + "papod_members".picture_uuid, + "papod_members".status + FROM "papod_members" + JOIN "papod_users" ON + "papod_users".id = "papod_members".user_id + JOIN "papod_networks" ON + "papod_networks".id = "papod_members".network_id + WHERE + "papod_members".user_id = ? AND + "papod_members".network_id = ? AND + "papod_members".status = 'active' AND + "papod_users".deleted = false AND + "papod_networks".deleted = false; + + -- addMember.sql: -- write: - -- FIXME papod + WITH target_user AS ( + SELECT id, username, display_name, picture_uuid + FROM "papod_users" + WHERE user_uuid = ? AND deleted = false + ), target_network AS ( + SELECT "papod_members".network_id AS id + FROM "papod_members" + JOIN "papod_networks" ON + "papod_members".network_id = "papod_networks".id + WHERE + "papod_members".id = ? AND + "papod_members".status = 'active' AND + "papod_networks".deleted = false + ) + INSERT INTO "papod_members" ( + uuid, network_id, user_id, username, display_name, + picture_uuid, status, active_uniq + ) VALUES ( + ?, + (SELECT id FROM target_network), + (SELECT id FROM target_user), + ?, + (SELECT display_name FROM target_user), + (SELECT picture_uuid FROM target_user), + 'active', + 'active' + ) RETURNING id, timestamp, display_name, picture_uuid, status, ( + SELECT CASE WHEN EXISTS ( + SELECT role from "papod_member_roles" + WHERE + member_id = ? AND + role IN ( + 'admin', + 'add-member' + ) + ) THEN true ELSE RAISE( + ABORT, + 'member not allowed to add another member' + ) END + ); + + +-- read: + +-- addRole.sql: +-- write: + INSERT INTO "papod_member_roles" (member_id, role) + VALUES (?, ?); + + +-- read: + +-- dropRole.sql: +-- write: + DELETE FROM "papod_member_roles" + WHERE + member_id = ? AND + role = ? + RETURNING 1; -- read: @@ -373,52 +944,175 @@ -- write: -- read: - -- FIXME papod + WITH current_network AS ( + SELECT network_id + FROM "papod_members" + WHERE id = ? + ) + SELECT + id, + timestamp, + username, + display_name, + picture_uuid, + status + FROM "papod_members" + WHERE + uuid = ? AND + network_id IN current_network; -- members.sql: -- write: -- read: - -- FIXME papod + WITH target_network AS ( + SELECT "papod_members".network_id + FROM "papod_members" + JOIN "papod_networks" ON + "papod_members".network_id = "papod_networks".id + WHERE + "papod_members".id = ? AND + "papod_networks".deleted = false + ) + SELECT + id, + timestamp, + uuid, + username, + display_name, + picture_uuid, + status + FROM "papod_members" + WHERE + network_id IN target_network AND + status = 'active'; -- editMember.sql: -- write: - -- FIXME papod + UPDATE "papod_members" + SET + status = ? + WHERE id = ? + RETURNING id; -- read: -- dropMember.sql: -- write: - -- FIXME + UPDATE "papod_members" SET status = 'removed' + WHERE uuid = ? RETURNING id; + + DELETE FROM "papod_member_roles" + WHERE + role != 'creator' AND + member_id IN ( + SELECT id FROM "papod_members" + WHERE uuid = ? + ) -- read: -- addChannel.sql: -- write: + WITH target_network AS ( + SELECT network_id AS id + FROM "papod_members" + WHERE id = ? + ) INSERT INTO "papod_channels" ( - uuid, public_name, label, description, virtual - ) VALUES (?, ?, ?, ?, ?) RETURNING id, timestamp; + uuid, + network_id, + public_name, + label, + description, + virtual + ) VALUES ( + ?, + (SELECT id FROM target_network), + ?, + ?, + ?, + ? + ) RETURNING id, timestamp; + + WITH new_channel AS ( + SELECT id FROM "papod_channels" WHERE uuid = ? + ) + INSERT INTO "papod_participants" (channel_id, member_id) + VALUES ( + (SELECT id FROM new_channel), + ? + ); -- read: + SELECT id, timestamp FROM "papod_channels" + WHERE uuid = ?; + -- channels.sql: -- write: -- read: - -- FIXME papod + WITH current_network AS ( + SELECT network_id AS id + FROM "papod_members" + WHERE id = ? + ), member_private_channels AS ( + SELECT channel_id AS id + FROM "papod_participants" + WHERE member_id = ? + ) + SELECT + id, + timestamp, + uuid, + public_name, + label, + description, + virtual + FROM "papod_channels" + WHERE + network_id IN current_network AND + ( + public_name IS NOT NULL OR + id IN member_private_channels + ) + ORDER BY id; --- topic.sql: +-- setChannel.sql: -- write: - -- FIXME papod + WITH participant_channel AS ( + SELECT channel_id AS id + FROM "papod_participants" + WHERE + member_id = ? AND + channel_id = ? + ) + UPDATE "papod_channels" + SET + description = ?, + public_name = ? + WHERE id IN participant_channel + RETURNING id; -- read: + SELECT ( + SELECT network_id AS id + FROM "papod_channels" + WHERE id = ? + ) AS channel_network_id, ( + SELECT network_id AS id + FROM "papod_members" + WHERE id = ? + ) AS member_network_id; + -- endChannel.sql: -- write: @@ -429,14 +1123,47 @@ -- join.sql: -- write: - -- FIXME papod + WITH target_channel AS ( + SELECT id + FROM "papod_channels" + WHERE + uuid = ? AND + public_name IS NOT NULL + ) + INSERT INTO "papod_participants" (channel_id, member_id) + VALUES ( + (SELECT id FROM target_channel), + ? + ) RETURNING id; -- read: + SELECT ( + SELECT network_id AS id + FROM "papod_channels" + WHERE + uuid = ? AND + public_name IS NOT NULL + ) AS channel_network_id, ( + SELECT network_id AS id + FROM "papod_members" WHERE id = ? + ) AS member_network_id; + -- part.sql: -- write: - -- FIXME papod + WITH target_channel AS ( + SELECT id + FROM "papod_channels" + WHERE + id = ? AND + virtual = false + ) + DELETE FROM "papod_participants" + WHERE + member_id = ? AND + channel_id IN target_channel + RETURNING 1; -- read: @@ -451,12 +1178,16 @@ -- addEvent.sql: -- write: INSERT INTO "papod_channel_events" ( - uuid, channel_id, connection_uuid, type, payload + uuid, channel_id, source_uuid, source_type, + source_metadata, type, payload, metadata ) VALUES ( ?, (SELECT id FROM "papod_channels" WHERE uuid = ?), ?, ?, + ?, + ?, + ?, ? ) RETURNING id, timestamp; @@ -477,7 +1208,7 @@ "papod_channel_events".timestamp, "papod_channel_events".uuid, "papod_channels".uuid, - "papod_channel_events".connection_uuid, + -- "papod_channel_events".connection_uuid, "papod_channel_events".type, "papod_channel_events".payload FROM "papod_channel_events" |