Trigger and function to insert user id into another table - postgresql

I am using Prisma as my schema and migrating it to supabase with prisma migrate dev
One of my tables Profiles, should reference the auth.users table in supabase, in sql something like this id uuid references auth.users not null,
Now since that table is automatically created in supabase do I still add it to my prisma schema? It's not in public either it is in auth.
model Profiles {
id String #id #db.Uuid
role String
subId String
stripeCustomerId String
refundId String[]
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
}
The reason I want the relation is because I want a trigger to automatically run a function that inserts an id and role into the profiles table when a new users is invited.
This is that trigger and function
-- inserts a row into public.profiles
create function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.Profiles (id, role)
values (new.id, 'BASE_USER');
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
I had this working when I created the profiles table manually in supabase I included the reference to the auth.users, that's the only reason I can think of why the user Id and role won't insert into the profiles db when I invite a user, the trigger and function are failing
create table public.Profiles (
id uuid references auth.users not null,
role text,
primary key (id)
);
Update from comment:
One error I found is
relation "public.profiles" does not exist
I change it to "public.Profiles" with a capital in supabase, but the function seem to still be looking for lowercase.

What you show should just work:
db<>fiddle here
Looks like you messed up capitalization with Postgres identifiers.
If you (or your ORM) created the table as "Profiles" (with double-quotes), non-standard capitalization is preserved and you need to double-quote the name for the rest of its life.
So the trigger function body must read:
...
insert into public."Profiles" (id, role) -- with double-quotes
...
Note that schema and table (and column) have to be quoted separately.
See:
Are PostgreSQL column names case-sensitive?

Related

Immutability in Postgres

I want to create an immutable Postgres database, where the user can insert & select (write & read) data, but cannot update or delete the data.
I am aware of the FOR UPDATE lock, but I don't understand how to use it.
Let's say for example I have the table below, how can I make it immutable (or, if I understood correctly, how can I use the FOR UPDATE lock permanently)
CREATE TABLE account(
user_id serial PRIMARY KEY,
username VARCHAR (50) UNIQUE NOT NULL,
password VARCHAR (50) NOT NULL,
email VARCHAR (355) UNIQUE NOT NULL,
created_on TIMESTAMP NOT NULL,
last_login TIMESTAMP
);
The solution is to give the user that accesses the database only the INSERT and SELECT privilege on the tables involved.
A lock is not a tool to deny somebody access, but a short-time barrier to prevent conflicting data modifications to happen at the same time.
Here is an example:
CREATE TABLE sensitive (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
available text,
restricted text
);
Now I want to allow someuser to insert data and read and update all columns except restricted, and I want to keep myself from deleting data in that table:
/* the CREATE TABLE above was run by user "laurenz" */
REVOKE DELETE ON sensitive FROM laurenz;
GRANT INSERT ON sensitive TO someuser;
GRANT SELECT (id, available), UPDATE (id, available) ON sensitive TO someuser;
Nope, that 👆🏼 solution doesn't work. I found this one. I make a before trigger on the table on update for each row:
create or replace function table_update_guard() returns trigger
language plpgsql immutable parallel safe cost 1 as $body$
begin
raise exception
'trigger %: updating is prohibited for %.%',
tg_name, tg_table_schema, tg_table_name
using errcode = 'restrict_violation';
return null;
end;
$body$;
create or replace trigger account_update_guard
before update on account for each row
execute function table_update_guard();
See my original research.

PostgreSQL - how to pass a function's argument to a trigger?

I have tree tables in a database: users (user_id (auto increment), fname, lname), roles (role_id, role_desc) and users_roles (user_id, role_id). What I'd like to do is to have a function create_user_with_role. The function takes 3 arguments: first name, last name and role_id. The function inserts a new row into the users table and a new user_id is created automatically. Now I want to insert a new record to the users_roles table: user_id is the newly created value and the role_id is taken from the function's arguments list.
Is it possible to pass the role_id argument to an after insert trigger (defined on users table) so another automatic insert can be performed? Or can you suggest any other solution?
First #Pavel Stehule is right:
Don't try to pass parameters to triggers, ever!
Second, you just have to get the inserted id into a variable.
CREATE FUNCTION create_user_with_role(first_name text, last_name text, new_role_id integer)
RETURNS VOID AS $$
DECLARE
new_user_id integer;
BEGIN
INSERT INTO users (fname, lname) VALUES (first_name, last_name)
RETURNING id INTO new_user_id;
INSERT INTO users_roles (user_id, role_id)
VALUES (new_user_id, new_role_id);
END;$$ LANGUAGE plpgsql;
Obviously, this is completely inefficient if you want to insert multiple rows but that's another question ;)
When you need to pass any parameter to trigger, then there is clean, so your design is wrong. Usually triggers should to have check or audit functionality. Not more. You can use a function, and call function directly from your application. Don't try to pass parameters to triggers, ever! Another bad sign are artificial columns in table used just only for trigger parametrization. This is pretty bad design!

postgres update NEW variable before INSERT in a TRIGGER

I've two tables accounts and projects:
create table accounts (
id bigserial primary key,
slug text unique
);
create table projects (
id bigserial primary key,
account_id bigint not null references accounts (id),
name text
);
I want to be able to insert a new row into projects by specifying only account.slug (not account.id). What I'm trying to achieve is something like:
INSERT into projects (account_slug, name) values ('account_slug', 'project_name');
I thought about using a trigger (unfortunately it doesn't work):
create or replace function trigger_projects_insert() returns trigger as $$
begin
if TG_OP = 'INSERT' AND NEW.account_slug then
select id as account_id
from accounts as account
where account.slug = NEW.account_slug;
NEW.account_id = account_id;
-- we should also remove NEW.account_slug but don't know how
end if;
return NEW;
end;
$$ LANGUAGE plpgsql;
create trigger trigger_projects_insert before insert on projects
for each row execute procedure trigger_projects_insert();
What is the best way to achieve what I'm trying to do?
Is a trigger a good idea?
Is there any other solution?
WITH newacc AS (
INSERT INTO accounts (slug)
VALUES ('account_slug')
RETURNING id
)
INSERT INTO projects (account_id, name)
SELECT id, 'project_name'
FROM newacct;
If you are limited in the SQL you can use, another idea might be to define a view over both tables and create an INSTEAD OF INSERT trigger on the view that performs the two INSERTs on the underlying tables. Then an INSERT statement like the one in your question would work.

How to use the auto-generated primary key in the same row postrges

I am trying to make an insert statement into a table with the following schema
CREATE TABLE auth(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role VARCHAR(64)
);
I am using pgcrypto extension to generate uuids. Is it possible to append id to the role field while inserting a row in this table?
I am inserting in this table using
insert into auth (role) values ('admin');
I want to append the id generated to admin so that the role would look something like admin_12234-3453-3453-345-34534.
You need an insert trigger to do this:
CREATE FUNCTION admin_uuid() RETURNS trigger AS $$
BEGIN
NEW.role := NEW.role || NEW.id::text;
RETURN NEW;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE TRIGGER set_admin_uuid
BEFORE INSERT ON auth
FOR EACH ROW EXECUTE PROCEDURE admin_uuid();
I think there is no direct way to achieve this. But postgres allows defining rules on tables and views (see https://www.postgresql.org/docs/9.2/static/sql-createrule.html). You can create a new rule for insertion which creates the id and the role as desired.

Prevent update of column except by trigger

Is it possible to configure a Postgres database such that a specific column may only be updated by a trigger, while still allowing the trigger itself to be executed in response to an update by a role without permission to update that column? If so, how?
For example, given tables and a trigger like this:
CREATE TABLE a(
id serial PRIMARY KEY,
flag boolean NOT NULL DEFAULT TRUE,
data text NOT NULL
);
CREATE TABLE b(
id serial PRIMARY KEY,
updated_on DATE NOT NULL DEFAULT CURRENT_DATE,
a_id INTEGER NOT NULL,
FOREIGN KEY (a_id) references a(id)
);
CREATE FUNCTION update_aflag() RETURNS trigger AS $update_aflag$
BEGIN
UPDATE a
SET flag = FALSE
WHERE id = NEW.a_id;
RETURN NEW;
END;
$update_aflag$ LANGUAGE plpgsql;
CREATE TRIGGER update_aflag_trigger
AFTER INSERT ON b
FOR EACH ROW
EXECUTE PROCEDURE update_aflag()
;
I'd like to define a role which does not have permission to update a.flag directly using an UPDATE statement, but which may update flag indirectly via the trigger.
Yes, this is possible using a SECURITY DEFINER trigger function. The trigger function runs as a role that has the right to modify the flag column, but you don't GRANT that right to normal users. You should create the trigger function as the role that you'll grant the required rights to.
This requires that the application not run as the user that owns the tables, and of course not as a superuser.
You can GRANT column update rights to other columns to the user, just leave out the flag column. Note that GRANTing UPDATE on all columns then REVOKEing it on flag will not work, it's not the same thing.