Skip to content

Commit

Permalink
feat: conversation roles and permissions
Browse files Browse the repository at this point in the history
there are a lot of changes in this commit but the roles and permissions are the most major.
- added scripts to reset the developer database and keydb.
- removed console.log of the database url in migrations.
- removed unnecessary relationNames in the db schema.
- added common helper which has a throwError function.
- added conversation helper for common functions.
- ensured that conversation queries/mutations are prefixed with "user".
- implemented adding and removing of participants.
- added defaults for limit and offset for getting messages.
- updated/added types to reflect the new roles and permissions.
  • Loading branch information
Creaous committed Dec 14, 2024
1 parent c9191c1 commit d525baa
Show file tree
Hide file tree
Showing 18 changed files with 577 additions and 236 deletions.
6 changes: 6 additions & 0 deletions dev/reset-database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

rm -f drizzle/\*.sql
rm -rf drizzle/meta
docker exec -ti nova-postgres dropdb -U postgres -f 'nova'
docker exec -ti nova-postgres createdb -U postgres 'nova'
3 changes: 3 additions & 0 deletions dev/reset-keydb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

docker exec -ti nova-keydb redis-cli flushall
2 changes: 0 additions & 2 deletions drizzle/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { sql } from 'drizzle-orm';

const migrationsFolder = process.argv[2] ?? '../drizzle';

console.log(Bun.env.DATABASE_URL);

export const dbClient = new Client({
connectionString: Bun.env.DATABASE_URL as string
});
Expand Down
9 changes: 4 additions & 5 deletions src/drizzle/schema/post/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,15 @@ export const post = pgTable('post', {
export const postRelations = relations(post, ({ one, many }) => ({
author: one(user, {
fields: [post.authorId],
references: [user.id],
relationName: 'posts'
references: [user.id]
}),
parent: one(post, {
relationName: 'post_parent_replies',
fields: [post.parentId],
references: [post.id],
relationName: 'replies'
references: [post.id]
}),
replies: many(post, {
relationName: 'replies'
relationName: 'post_parent_replies'
}),
interactions: many(postInteraction),
editHistory: many(postEditHistory),
Expand Down
84 changes: 76 additions & 8 deletions src/drizzle/schema/user/Conversation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InferSelectModel, relations } from 'drizzle-orm';
import { InferSelectModel, relations, sql } from 'drizzle-orm';
import {
boolean,
pgEnum,
pgTable,
primaryKey,
Expand All @@ -14,7 +15,7 @@ export const userConversationType = pgEnum('user_conversation_type', [
]);

export const userConversation = pgTable('user_conversation', {
id: uuid('id').defaultRandom().primaryKey(),
id: uuid('id').defaultRandom().notNull().primaryKey(),
name: citext('name'),
type: userConversationType('conversation_type').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow()
Expand All @@ -24,16 +25,15 @@ export const userConversationRelations = relations(
userConversation,
({ many }) => ({
messages: many(userConversationMessage),
participants: many(userConversationParticipant, {
relationName: 'user_conversation_participants'
})
participants: many(userConversationParticipant),
roles: many(userConversationRole)
})
);

export const userConversationMessage = pgTable(
'user_conversation_message',
{
id: uuid('id').defaultRandom(),
id: uuid('id').defaultRandom().notNull(),
conversationId: uuid('conversation_id').notNull(),
senderId: citext('sender_id').notNull(),
content: citext('content').notNull(),
Expand Down Expand Up @@ -71,6 +71,7 @@ export const userConversationMessageRelations = relations(
export const userConversationParticipant = pgTable(
'user_conversation_participant',
{
id: uuid('id').defaultRandom().notNull(),
conversationId: uuid('conversation_id').notNull(),
userId: citext('user_id').notNull(),
joinedAt: timestamp('joined_at').notNull().defaultNow()
Expand All @@ -90,12 +91,73 @@ export const userConversationParticipantRelations = relations(
messages: many(userConversationMessage),
conversation: one(userConversation, {
fields: [userConversationParticipant.conversationId],
references: [userConversation.id],
relationName: 'user_conversation_participants'
references: [userConversation.id]
}),
user: one(user, {
fields: [userConversationParticipant.userId],
references: [user.id]
}),
roles: many(userConversationParticipantRole)
})
);

export const userConversationRole = pgTable(
'user_conversation_role',
{
id: uuid('id').defaultRandom().notNull(),
name: citext('name').notNull(),
description: citext('description').notNull(),
conversationId: uuid('conversation_id').notNull(),
default: boolean('default').notNull().default(false),
permissions: citext('permissions')
.notNull()
.default(sql`'{}'::citext[]`)
},
(t) => {
return {
pk: primaryKey({
columns: [t.id, t.conversationId]
})
};
}
);

export const userConversationRoleRelations = relations(
userConversationRole,
({ one, many }) => ({
conversation: one(userConversation, {
fields: [userConversationRole.conversationId],
references: [userConversation.id]
}),
members: many(userConversationParticipantRole)
})
);

export const userConversationParticipantRole = pgTable(
'user_conversation_participant_role',
{
participantId: uuid('participant_id').notNull(),
roleId: uuid('role_id').notNull()
},
(t) => {
return {
pk: primaryKey({
columns: [t.participantId, t.roleId]
})
};
}
);

export const userConversationParticipantRoleRelations = relations(
userConversationParticipantRole,
({ one }) => ({
participant: one(userConversationParticipant, {
fields: [userConversationParticipantRole.participantId],
references: [userConversationParticipant.id]
}),
role: one(userConversationRole, {
fields: [userConversationParticipantRole.roleId],
references: [userConversationRole.id]
})
})
);
Expand All @@ -106,6 +168,12 @@ export type UserConversationSchemaType = InferSelectModel<
export type UserConversationMessageSchemaType = InferSelectModel<
typeof userConversationMessage
>;
export type UserConversationRoleSchemaType = InferSelectModel<
typeof userConversationRole
>;
export type UserConversationParticipantSchemaType = InferSelectModel<
typeof userConversationParticipant
>;
export type UserConversationParticipantRoleSchemaType = InferSelectModel<
typeof userConversationParticipantRole
>;
8 changes: 2 additions & 6 deletions src/drizzle/schema/user/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,14 @@ export const userRelations = relations(user, ({ one, many }) => ({
fromRelationships: many(userRelationship, {
relationName: 'user_from_relationships'
}),
posts: many(post, {
relationName: 'posts'
}),
posts: many(post),
postInteraction: many(postInteraction),
verification: one(userVerification),
profileFields: many(userProfileField),
ownedPlanets: many(userPlanet),
joinedPlanets: many(userPlanetMember),
settings: many(userSetting),
conversations: many(userConversationParticipant, {
relationName: 'user_conversation_participants'
}),
conversations: many(userConversationParticipant),
collections: many(postCollection)
}));

Expand Down
7 changes: 7 additions & 0 deletions src/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GraphQLError } from 'graphql';

const throwError = (message: string, code: string) => {
throw new GraphQLError(message, { extensions: { code } });
};

export { throwError };
73 changes: 73 additions & 0 deletions src/helpers/user/Conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { db } from '../../drizzle/db';
import { throwError } from '../common';

const getConversation = async (id: string) => {
const conversation = await db.query.userConversation.findFirst({
where: (userConversation, { eq }) => eq(userConversation.id, id)
});
if (!conversation)
throwError(
'The conversation does not exist.',
'CONVERSATION_NOT_FOUND'
);
return conversation;
};

const getParticipant = async (userId: string, conversationId: string) => {
await getConversation(conversationId);
const participant = await db.query.userConversationParticipant.findFirst({
where: (userConversationParticipant, { and, eq }) =>
and(
eq(userConversationParticipant.conversationId, conversationId),
eq(userConversationParticipant.userId, userId)
),
with: {
roles: {
with: {
role: {
with: {
members: true
}
}
}
}
}
});
if (!participant)
throwError(
'You must be a participant in this conversation to proceed with this action.',
'CONVERSATION_PARTICIPANT_REQUIRED'
);
return participant;
};

const getPermissions = async (conversationId: string, userId: string) => {
const participant = await getParticipant(userId, conversationId);
var perms = [];
for (const role of participant?.roles ?? []) {
for (const permission of JSON.parse(role.role.permissions)) {
perms.push(permission);
}
}
return perms;
};

const checkPermissions = async (
permissions: string[],
conversationId: string,
userId: string
) => {
const _permissions = await getPermissions(conversationId, userId);
const missing = permissions.filter((p) => !_permissions.includes(p));

if (missing.length > 0) {
throwError(
`You do not have permission to proceed with this action. Missing permission(s): ${missing.join(
', '
)}`,
'CONVERSATION_PERMISSIONS_MISSING'
);
}
};

export { getConversation, getParticipant, getPermissions, checkPermissions };
Loading

0 comments on commit d525baa

Please sign in to comment.