-- -- acs-kernel/sql/community-core-create.sql -- -- Abstractions fundamental to any online community (or information -- system, in general), derived in large part from the ACS 3.x -- community-core data model by Philip Greenspun (philg@mit.edu), from -- the ACS 3.x user-groups data model by Tracy Adams (teadams@mit.edu) -- from Chapter 3 (The Enterprise and Its World) of David Hay's -- book _Data_Model_Patterns_, and from Chapter 2 (Accountability) -- of Martin Fowler's book _Analysis_Patterns_. -- -- @author Michael Yoon (michael@arsdigita.com) -- @author Rafael Schloming (rhs@mit.edu) -- @author Jon Salz (jsalz@mit.edu) -- -- @creation-date 2000-05-18 -- -- @cvs-id $Id: community-core-create.sql,v 1.31 2024/09/11 06:15:48 gustafn Exp $ -- -- HIGH PRIORITY: -- -- * What can subtypes add to the specification of supertype -- attributes? Extra constraints like "not null"? What about -- "storage"? Can a subtype override how a given attribute is -- stored? -- -- * Can we realistically revoke INSERT and UPDATE permission on the -- tables (users, persons, etc.) and make people use the PL/SQL API? -- One downside is that it would then be difficult or impossible to -- do things like "update ... set ... where ..." directly, without -- creating a PL/SQL procedure to do it. -- -- * Figure out how to migrate from ACS 3.x users, user_groups, -- user_group_types, etc. to ACS 4.0 objects/parties/users/organizations; -- also need to consider general_* and site_wide_* tables -- (Rafi and Luke) -- -- * Take an inventory of acs-kernel tables and other objects (some of which -- may still be in /www/doc/sql/) and create their ACS 4 analogs, including -- mapping over all general_* and site_wide_* data models, and make -- appropriate adjustments to code -- (Rafi and Yon/Luke/?). -- -- * Create magic users: system and anonymous (do we actually need these?) -- -- * Define and implement APIs -- -- * Figure out user classes, e.g., treat "the set of parties that -- have relationship X to object Y" as a party in its own right -- -- * Explain why acs_rel_types, acs_rel_rules, and acs_rels are not -- merely replicating the functionality of a relational database. -- -- * acs_attribute_type should impose some rules on the min_n_values -- and max_n_values columns of acs_attributes, e.g., it doesn't -- really make sense for a boolean attribute to have more than -- one value -- -- * Add support for default values to acs_attributes. -- -- * Add support for instance-specific attributes (e.g., -- user_group_member_fields) -- -- MEDIUM PRIORITY: -- -- * Read-only attributes? -- -- * Do we need to store metadata about enumerations and valid ranges -- or should we query the Oracle data dictionary for info on check -- constraints? -- -- * Create a "user_group_type" (an object_type with "organization" -- as its supertype (do we need this?) -- -- * Add in ancestor permission view, assuming that we'll use a -- magical rel_type: "acs_acl"? -- -- * How do we get all attribute values for objects of a specific -- type? "We probably want some convention or standard way for -- providing a view that joins supertypes and a type. This could -- be automatically generated through metadata, or it could simply -- be a convention." - Rafi -- -- LOW PRIORITY: -- -- * Formalize Rafi's definition of an "object": "A collection of rows -- identified by an object ID for which we maintain metadata" or -- something like that. -- -- * "We definitely need some standard way of extending a supertype into -- a subtype, and 'deleting' a subtype into a supertype. This will be -- needed when we want to transform a 'person' into a registered -- user, and do 'nuke user' but keep around the user's contributed -- content and associate it with the 'person' part of that user. This -- actually works quite nicely with standard oracle inheritance since -- you can just insert or delete a row in the subtype table and -- mutate the object type." - Rafi -- -- ACS 4.1: -- -- * Figure out what to do with pretty names (message catalog) -- -- COMPLETED: -- -- * Create magic parties: all_users (or just use null party_id?) -- and registered_users -- -- * Test out relationship attributes (making "relationship" an -- acs_object_type) -- -- * Create magic object_types (object, party, person, user, -- organization) including attrs and rels -- -- * Create constraints for creation_user and modifying_user -- -- procedure inline_0/0 -- CREATE OR REPLACE FUNCTION inline_0( ) RETURNS integer AS $$ DECLARE attr_id acs_attributes.attribute_id%TYPE; BEGIN -- -- Party: the supertype of person and organization -- PERFORM acs_object_type__create_type ( 'party', 'Party', 'Parties', 'acs_object', 'parties', 'party_id', 'party', 'f', null, 'party__name' ); attr_id := acs_attribute__create_attribute ( 'party', 'email', 'string', 'Email Address', 'Email Addresses', null, null, null, 0, 1, null, 'type_specific', 'f' ); attr_id := acs_attribute__create_attribute ( 'party', 'url', 'string', 'URL', 'URLs', null, null, null, 0, 1, null, 'type_specific', 'f' ); -- -- Person: the supertype of user -- attr_id := acs_object_type__create_type ( 'person', 'Person', 'People', 'party', 'persons', 'person_id', 'person', 'f', null, 'person__name' ); attr_id := acs_attribute__create_attribute ( 'person', 'first_names', 'string', 'First Names', 'First Names', null, null, null, 0, 1, null, 'type_specific', 'f' ); attr_id := acs_attribute__create_attribute ( 'person', 'last_name', 'string', 'Last Name', 'Last Names', null, null, null, 0, 1, null, 'type_specific', 'f' ); -- -- User: people who have registered in the system -- attr_id := acs_object_type__create_type ( 'user', 'User', 'Users', 'person', 'users', 'user_id', 'acs_user', 'f', null, null ); attr_id := acs_attribute__create_attribute ( 'user', 'username', 'string', 'Username', 'Usernames', null, null, null, 0, 1, null, 'type_specific', 'f' ); attr_id := acs_attribute__create_attribute ( 'user', 'screen_name', 'string', 'Screen Name', 'Screen Names', null, null, null, 0, 1, null, 'type_specific', 'f' ); attr_id := acs_attribute__create_attribute ( 'person', 'bio', 'string', 'Biography', 'Biographies', null, null, null, 0, 1, null, 'type_specific', 'f' ); return 0; END; $$ LANGUAGE plpgsql; select inline_0 (); drop function inline_0 (); -- show errors -- ****************************************************************** -- * OPERATIONAL LEVEL -- ****************************************************************** create table parties ( party_id integer not null constraint parties_party_id_fk references acs_objects (object_id) constraint parties_party_id_pk primary key, email varchar(100) constraint parties_email_un unique, url varchar(200) ); create index parties_email_lower_idx on parties(lower(email)); comment on table parties is ' Party is the supertype of person and organization. It exists because many other types of object can have relationships to parties. '; comment on column parties.url is ' We store url here so that we can always make party names hyperlinks without joining to any other table. '; ------------------- -- PARTY PACKAGE -- ------------------- -- added select define_function_args('party__new','party_id;null,object_type;party,creation_date;now(),creation_user;null,creation_ip;null,email,url;null,context_id;null'); -- -- procedure party__new/8 -- CREATE OR REPLACE FUNCTION party__new( new__party_id integer, -- default null new__object_type varchar, -- default 'party' new__creation_date timestamptz, -- default now() new__creation_user integer, -- default null new__creation_ip varchar, -- default null new__email varchar, new__url varchar, -- default null new__context_id integer -- default null ) RETURNS integer AS $$ DECLARE v_party_id parties.party_id%TYPE; BEGIN v_party_id := acs_object__new(new__party_id, new__object_type, new__creation_date, new__creation_user, new__creation_ip, new__context_id, 't', new__email, null); insert into parties (party_id, email, url) values (v_party_id, lower(new__email), new__url); return v_party_id; END; $$ LANGUAGE plpgsql; -- added select define_function_args('party__delete','party_id'); -- -- procedure party__delete/1 -- CREATE OR REPLACE FUNCTION party__delete( party_id integer ) RETURNS integer AS $$ DECLARE BEGIN PERFORM acs_object__delete(party_id); return 0; END; $$ LANGUAGE plpgsql; -- added select define_function_args('party__name','party_id'); -- -- procedure party__name/1 -- CREATE OR REPLACE FUNCTION party__name( party_id integer ) RETURNS varchar AS $$ DECLARE BEGIN if party_id = -1 then return 'The Public'; else return null; end if; END; $$ LANGUAGE plpgsql immutable strict; -- added select define_function_args('party__email','party_id'); -- -- procedure party__email/1 -- CREATE OR REPLACE FUNCTION party__email( email__party_id integer ) RETURNS varchar AS $$ DECLARE BEGIN return email from parties where party_id = email__party_id; END; $$ LANGUAGE plpgsql stable strict; -- show errors ------------- -- PERSONS -- ------------- create table persons ( person_id integer not null constraint persons_person_id_fk references parties (party_id) constraint persons_person_id_pk primary key, first_names varchar(100) not null, last_name varchar(100) not null, bio text ); comment on table persons is ' Need to handle titles like Mr., Ms., Mrs., Dr., etc. and suffixes like M.D., Ph.D., Jr., Sr., III, IV, etc. '; -------------------- -- PERSON PACKAGE -- -------------------- -- create or replace package person -- as -- -- function new ( -- person_id in persons.person_id%TYPE default null, -- object_type in acs_objects.object_type%TYPE -- default 'person', -- creation_date in acs_objects.creation_date%TYPE -- default sysdate, -- creation_user in acs_objects.creation_user%TYPE -- default null, -- creation_ip in acs_objects.creation_ip%TYPE default null, -- email in parties.email%TYPE, -- url in parties.url%TYPE default null, -- first_names in persons.first_names%TYPE, -- last_name in persons.last_name%TYPE, -- context_id in acs_objects.context_id%TYPE default null -- ) return persons.person_id%TYPE; -- -- procedure delete ( -- person_id in persons.person_id%TYPE -- ); -- -- function name ( -- person_id in persons.person_id%TYPE -- ) return varchar2; -- -- end person; -- show errors -- create or replace package body person -- function new -- old define_function_args('person__new','person_id,object_type;person,creation_date;now(),creation_user,creation_ip,email,url,first_names,last_name,context_id') -- new select define_function_args('person__new','person_id;null,object_type;person,creation_date;now(),creation_user;null,creation_ip;null,email,url;null,first_names,last_name,context_id;null'); -- -- procedure person__new/10 -- CREATE OR REPLACE FUNCTION person__new( new__person_id integer, -- default null new__object_type varchar, -- default 'person' new__creation_date timestamptz, -- default now() -- default 'now()' new__creation_user integer, -- default null new__creation_ip varchar, -- default null new__email varchar, new__url varchar, -- default null new__first_names varchar, new__last_name varchar, new__context_id integer -- default null ) RETURNS integer AS $$ DECLARE v_person_id persons.person_id%TYPE; BEGIN v_person_id := party__new(new__person_id, new__object_type, new__creation_date, new__creation_user, new__creation_ip, new__email, new__url, new__context_id); update acs_objects set title = new__first_names || ' ' || new__last_name where object_id = v_person_id; insert into persons (person_id, first_names, last_name) values (v_person_id, new__first_names, new__last_name); return v_person_id; END; $$ LANGUAGE plpgsql; -- procedure delete -- added select define_function_args('person__delete','person_id'); -- -- procedure person__delete/1 -- CREATE OR REPLACE FUNCTION person__delete( delete__person_id integer ) RETURNS integer AS $$ DECLARE BEGIN delete from persons where person_id = delete__person_id; PERFORM party__delete(delete__person_id); return 0; END; $$ LANGUAGE plpgsql; -- function name -- added select define_function_args('person__name','person_id'); -- -- procedure person__name/1 -- CREATE OR REPLACE FUNCTION person__name( name__person_id integer ) RETURNS varchar AS $$ DECLARE BEGIN return first_names || ' ' || last_name from persons where person_id = name__person_id; END; $$ LANGUAGE plpgsql stable strict; -- function first_names -- added select define_function_args('person__first_names','person_id'); -- -- procedure person__first_names/1 -- CREATE OR REPLACE FUNCTION person__first_names( first_names__person_id integer ) RETURNS varchar AS $$ DECLARE BEGIN return first_names from persons where person_id = first_names__person_id; END; $$ LANGUAGE plpgsql stable strict; -- function last_name -- added select define_function_args('person__last_name','person_id'); -- -- procedure person__last_name/1 -- CREATE OR REPLACE FUNCTION person__last_name( last_name__person_id integer ) RETURNS varchar AS $$ DECLARE BEGIN return last_name from persons where person_id = last_name__person_id; END; $$ LANGUAGE plpgsql stable strict; -- show errors create table users ( user_id integer not null constraint users_user_id_fk references persons (person_id) constraint users_user_id_pk primary key, authority_id integer constraint users_authority_id_fk references auth_authorities(authority_id), username varchar(100) constraint users_username_nn not null, screen_name varchar(100) constraint users_screen_name_un unique, priv_name integer default 0 not null, priv_email integer default 5 not null, email_verified_p boolean default 't', email_bouncing_p boolean default 'f' not null, last_visit timestamptz, second_to_last_visit timestamptz, n_sessions integer default 1 not null, -- local authentication information password character varying(128), password_hash_algorithm character varying(100) DEFAULT 'salted-sha1' NOT NULL, salt char(40), password_question varchar(1000), password_answer varchar(1000), password_changed_date timestamptz, -- used for the authentication cookie auth_token varchar(100), -- table constraints constraint users_authority_username_un unique (authority_id, username) ); create index users_username_lower_idx on users(lower(username)); create index users_screenname_lower_idx on users(lower(screen_name)); create table user_preferences ( user_id integer constraint user_preferences_user_id_fk references users (user_id) constraint user_preferences_user_id_pk primary key, prefer_text_only_p boolean default 'f', -- an ISO 639 language code (in lowercase) language_preference char(2) default 'en', dont_spam_me_p boolean default 'f', email_type varchar(64), timezone varchar(100) ); CREATE OR REPLACE FUNCTION inline_1 () RETURNS integer AS $$ BEGIN insert into acs_object_type_tables (object_type, table_name, id_column) values ('user', 'user_preferences', 'user_id'); return 0; END; $$ LANGUAGE plpgsql; select inline_1 (); drop function inline_1 (); -- show errors alter table acs_objects add constraint acs_objects_creation_user_fk foreign key (creation_user) references users(user_id); alter table acs_objects add constraint acs_objects_modifying_user_fk foreign key (modifying_user) references users(user_id); comment on table users is ' The creation_date and creation_ip columns inherited from acs_objects indicate when and from where the user registered. How do we apply a constraint ("email must not be null") to the parent type? '; comment on column users.last_visit is ' Set when user reappears at site '; comment on column users.second_to_last_visit is ' This is what most pages query against (since last_visit will only be a few minutes old for most pages in a session) '; comment on column users.n_sessions is ' How many times this user has visited '; ---------------------- -- ACS_USER PACKAGE -- ---------------------- -- create or replace package acs_user -- as -- -- function new ( -- user_id in users.user_id%TYPE default null, -- object_type in acs_objects.object_type%TYPE -- default 'user', -- creation_date in acs_objects.creation_date%TYPE -- default sysdate, -- creation_user in acs_objects.creation_user%TYPE -- default null, -- creation_ip in acs_objects.creation_ip%TYPE default null, -- email in parties.email%TYPE, -- url in parties.url%TYPE default null, -- first_names in persons.first_names%TYPE, -- last_name in persons.last_name%TYPE, -- password in users.password%TYPE, -- salt in users.salt%TYPE, -- password_question in users.password_question%TYPE default null, -- password_answer in users.password_answer%TYPE default null, -- screen_name in users.screen_name%TYPE default null, -- email_verified_p in users.email_verified_p%TYPE default 't', -- context_id in acs_objects.context_id%TYPE default null -- ) -- return users.user_id%TYPE; -- -- procedure approve_email ( -- user_id in users.user_id%TYPE -- ); -- -- procedure unapprove_email ( -- user_id in users.user_id%TYPE -- ); -- -- procedure delete ( -- user_id in users.user_id%TYPE -- ); -- -- end acs_user; -- show errors -- create or replace package body acs_user select define_function_args('acs_user__new','user_id;null,object_type;user,creation_date;now(),creation_user;null,creation_ip;null,authority_id;null,username,email,url;null,first_names,last_name,password,salt,screen_name;null,email_verified_p;t,context_id;null'); -- -- procedure acs_user__new/16 -- CREATE OR REPLACE FUNCTION acs_user__new( p_user_id integer, -- default null p_object_type varchar, -- default 'user' p_creation_date timestamptz, -- default now() p_creation_user integer, -- default null p_creation_ip varchar, -- default null p_authority_id integer, -- defaults to local authority p_username varchar, p_email varchar, p_url varchar, -- default null p_first_names varchar, p_last_name varchar, p_password char, p_salt char, p_screen_name varchar, -- default null p_email_verified_p boolean, -- default 't' p_context_id integer -- default null ) RETURNS integer AS $$ DECLARE v_user_id users.user_id%TYPE; v_authority_id auth_authorities.authority_id%TYPE; v_person_exists integer; BEGIN v_user_id := p_user_id; select 1 from persons into v_person_exists where person_id = v_user_id; if NOT FOUND then v_user_id := person__new( v_user_id, p_object_type, p_creation_date, p_creation_user, p_creation_ip, p_email, p_url, p_first_names, p_last_name, p_context_id ); else update acs_objects set object_type = 'user' where object_id = v_user_id; end if; -- default to local authority if p_authority_id is null then select authority_id into v_authority_id from auth_authorities where short_name = 'local'; else v_authority_id := p_authority_id; end if; insert into users (user_id, authority_id, username, password, salt, screen_name, email_verified_p) values (v_user_id, v_authority_id, p_username, p_password, p_salt, p_screen_name, p_email_verified_p); insert into user_preferences (user_id) values (v_user_id); return v_user_id; END; $$ LANGUAGE plpgsql; -- -- procedure acs_user__new/5 -- CREATE OR REPLACE FUNCTION acs_user__new( email varchar, fname varchar, lname varchar, pword char, salt char ) RETURNS integer AS $$ DECLARE BEGIN return acs_user__new(null, 'user', now(), null, null, email, null, fname, lname, pword, salt, null, null, null, 't', null ); END; $$ LANGUAGE plpgsql; -- procedure approve_email -- added select define_function_args('acs_user__approve_email','user_id'); -- -- procedure acs_user__approve_email/1 -- CREATE OR REPLACE FUNCTION acs_user__approve_email( approve_email__user_id integer ) RETURNS integer AS $$ DECLARE BEGIN update users set email_verified_p = 't' where user_id = approve_email__user_id; return 0; END; $$ LANGUAGE plpgsql; -- procedure unapprove_email -- added select define_function_args('acs_user__unapprove_email','user_id'); -- -- procedure acs_user__unapprove_email/1 -- CREATE OR REPLACE FUNCTION acs_user__unapprove_email( unapprove_email__user_id integer ) RETURNS integer AS $$ DECLARE BEGIN update users set email_verified_p = 'f' where user_id = unapprove_email__user_id; return 0; END; $$ LANGUAGE plpgsql; -- procedure delete -- added select define_function_args('acs_user__delete','user_id'); -- -- procedure acs_user__delete/1 -- CREATE OR REPLACE FUNCTION acs_user__delete( delete__user_id integer ) RETURNS integer AS $$ DECLARE BEGIN delete from user_preferences where user_id = delete__user_id; delete from users where user_id = delete__user_id; PERFORM person__delete(delete__user_id); return 0; END; $$ LANGUAGE plpgsql; -- show errors