Skip to content

Commit

Permalink
feat: very basic implementation of the conversations API
Browse files Browse the repository at this point in the history
  • Loading branch information
Creaous committed Dec 7, 2024
1 parent ca02737 commit f79588b
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 51 deletions.
83 changes: 50 additions & 33 deletions src/drizzle/schema/user/Conversation.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,82 @@
import { relations } from 'drizzle-orm';
import { InferSelectModel, relations } from 'drizzle-orm';
import { pgEnum, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core';
import { citext, user } from '..';

export const conversationType = pgEnum('conversation_type', [
export const userConversationType = pgEnum('user_conversation_type', [
'DIRECT',
'GROUP'
]);

export const conversation = pgTable('conversation', {
id: uuid('id').primaryKey(),
type: conversationType('conversation_type').notNull(),
export const userConversation = pgTable('user_conversation', {
id: uuid('id').defaultRandom().primaryKey(),
name: citext('name'),
type: userConversationType('conversation_type').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow()
});

export const conversationRelations = relations(conversation, ({ many }) => ({
messages: many(conversationMessage),
participants: many(conversationParticipant, {
relationName: 'conversation_participants'
export const userConversationRelations = relations(
userConversation,
({ many }) => ({
messages: many(userConversationMessage),
participants: many(userConversationParticipant, {
relationName: 'user_conversation_participants'
})
})
}));
);

export const conversationMessage = pgTable('conversation_message', {
id: uuid('id').primaryKey(),
export const userConversationMessage = pgTable('user_conversation_message', {
id: uuid('id').defaultRandom().primaryKey(),
conversationId: uuid('conversation_id').notNull(),
senderId: citext('sender_id').notNull(),
content: citext('content').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow()
});

export const conversationMessageRelations = relations(
conversationMessage,
export const userConversationMessageRelations = relations(
userConversationMessage,
({ one }) => ({
conversation: one(conversation, {
fields: [conversationMessage.conversationId],
references: [conversation.id]
conversation: one(userConversation, {
fields: [userConversationMessage.conversationId],
references: [userConversation.id]
}),
sender: one(conversationParticipant, {
fields: [conversationMessage.senderId],
references: [conversationParticipant.userId]
sender: one(userConversationParticipant, {
fields: [userConversationMessage.senderId],
references: [userConversationParticipant.userId]
})
})
);

export const conversationParticipant = pgTable('conversation_participant', {
conversationId: uuid('conversation_id').notNull(),
userId: citext('user_id').notNull(),
joinedAt: timestamp('joined_at').notNull().defaultNow()
});
export const userConversationParticipant = pgTable(
'user_conversation_participant',
{
conversationId: uuid('conversation_id').notNull(),
userId: citext('user_id').notNull(),
joinedAt: timestamp('joined_at').notNull().defaultNow()
}
);

export const conversationParticipantRelations = relations(
conversationParticipant,
export const userConversationParticipantRelations = relations(
userConversationParticipant,
({ one, many }) => ({
messages: many(conversationMessage),
conversation: one(conversation, {
fields: [conversationParticipant.conversationId],
references: [conversation.id],
relationName: 'conversation_participants'
messages: many(userConversationMessage),
conversation: one(userConversation, {
fields: [userConversationParticipant.conversationId],
references: [userConversation.id],
relationName: 'user_conversation_participants'
}),
user: one(user, {
fields: [conversationParticipant.userId],
fields: [userConversationParticipant.userId],
references: [user.id]
})
})
);

export type UserConversationSchemaType = InferSelectModel<
typeof userConversation
>;
export type UserConversationMessageSchemaType = InferSelectModel<
typeof userConversationMessage
>;
export type UserConversationParticipantSchemaType = InferSelectModel<
typeof userConversationParticipant
>;
6 changes: 4 additions & 2 deletions src/drizzle/schema/user/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { InferSelectModel, relations, sql } from 'drizzle-orm';
import { boolean, pgEnum, pgTable, timestamp } from 'drizzle-orm/pg-core';
import {
citext,
conversationParticipant,
userConversationParticipant,
post,
postCollection,
postInteraction,
Expand Down Expand Up @@ -56,7 +56,9 @@ export const userRelations = relations(user, ({ one, many }) => ({
ownedPlanets: many(userPlanet),
joinedPlanets: many(userPlanetMember),
settings: many(userSetting),
conversations: many(conversationParticipant),
conversations: many(userConversationParticipant, {
relationName: 'user_conversation_participants'
}),
collections: many(postCollection)
}));

Expand Down
31 changes: 31 additions & 0 deletions src/mutations/user/Conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Context } from '../../context';
import { builder } from '../../builder';
import { db } from '../../drizzle/db';
import { userConversationMessage } from '../../drizzle/schema';
import { UserConversationMessage } from '../../types/user/conversation/Message';
import { pubsub } from '../../pubsub';

builder.mutationField('createConversationMessage', (t) =>
t.field({
type: UserConversationMessage,
args: {
content: t.arg.string({ required: true }),
conversationId: t.arg.string({ required: true })
},
resolve: async (_root, _args, ctx: Context) => {
pubsub.publish(
`userConversationMessages${_args.conversationId}`,
{}
);
return db
.insert(userConversationMessage)
.values({
content: _args.content,
conversationId: _args.conversationId,
senderId: '1' // ctx.oidc.sub
})
.returning()
.then((res) => res[0]);
}
})
);
1 change: 1 addition & 0 deletions src/mutations/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ProfileField';
export * from './Relationship';
export * from './Conversation';
32 changes: 32 additions & 0 deletions src/queries/user/Conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GraphQLError } from 'graphql';
import { builder } from '../../builder';
import { db } from '../../drizzle/db';
import { User } from '../../types';
import { Context } from '../../context';
import { UserConversationMessage } from '../../types/user/conversation/Message';

builder.queryField('userConversationMessages', (t) =>
t.field({
type: [UserConversationMessage],
args: {
id: t.arg.string({ required: true }),
limit: t.arg.int({ required: false }),
offset: t.arg.int({ required: false })
},
smartSubscription: true,
subscribe: (subscriptions, root, args) =>
subscriptions.register(`userConversationMessages${args.id}`),
resolve: async (_root, args, ctx: Context) => {
const messages = await db.query.userConversationMessage.findMany({
where: (userConversationMessage, { eq }) =>
eq(userConversationMessage.conversationId, args.id),
limit: args.limit!,
offset: args.offset!,
orderBy: (userConversationMessage, { desc }) =>
desc(userConversationMessage.createdAt)
});

return messages;
}
})
);
1 change: 1 addition & 0 deletions src/queries/user/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './User';
export * from './Conversation';
1 change: 1 addition & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import './types';

builder.queryType({});
builder.mutationType({});
builder.subscriptionType({});

export const schema = builder.toSchema();
16 changes: 0 additions & 16 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ export async function startServer() {
// TODO: Remove this and use event notifications instead.
// We are waiting on the Backblaze B2 team to allow us.
return mediaUploadEndpoint(req);
} else if (url.pathname === '/wstest') {
pubsub.publish('dbUpdatedUser', {});
return new Response();
} else if (url.pathname.startsWith('/webhook/')) {
return webhookEndpoint(req);
} else {
Expand Down Expand Up @@ -141,19 +138,6 @@ export async function startServer() {
console.log('🧪 Running in test mode');
}
console.log('\x1b[0m');

if (
(await db
.execute(
sql`SELECT * FROM pg_available_extensions WHERE name = 'citext';`
)
.then((r) => r.rows[0].installed_version)) === null
) {
console.log(
'🔑 Citext extension has not been found on the database. We will attempt to install it now.'
);
await db.execute(sql`CREATE EXTENSION IF NOT EXISTS citext;`);
}
}

if (!isTestMode) {
Expand Down
23 changes: 23 additions & 0 deletions src/types/user/conversation/Conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { User } from '../..';
import { builder } from '../../../builder';
import { db } from '../../../drizzle/db';
import { type UserConversationSchemaType } from '../../../drizzle/schema';

export const UserConversationType = builder.enumType('UserConversationType', {
values: ['DIRECT', 'GROUP']
});

export const UserConversation =
builder.objectRef<UserConversationSchemaType>('UserConversation');

UserConversation.implement({
fields: (t) => ({
id: t.exposeString('id', { nullable: false }),
name: t.exposeString('name', { nullable: true }),
type: t.expose('type', {
type: UserConversationType,
nullable: false
}),
createdAt: t.expose('createdAt', { type: 'Date', nullable: false })
})
});
46 changes: 46 additions & 0 deletions src/types/user/conversation/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { User, UserConversation } from '../..';
import { builder } from '../../../builder';
import { db } from '../../../drizzle/db';
import { type UserConversationMessageSchemaType } from '../../../drizzle/schema';
import { UserConversationParticipant } from './Participant';

export const UserConversationMessage =
builder.objectRef<UserConversationMessageSchemaType>(
'UserConversationMessage'
);

UserConversationMessage.implement({
fields: (t) => ({
conversation: t.field({
type: UserConversation,
nullable: false,
resolve: async (userConversationMessage) => {
const result = await db.query.userConversation.findFirst({
where: (userConversation, { eq }) =>
eq(
userConversation.id,
userConversationMessage.conversationId
)
});
return result!;
}
}),
sender: t.field({
type: UserConversationParticipant,
nullable: false,
resolve: async (userConversationMessage) => {
const result =
await db.query.userConversationParticipant.findFirst({
where: (userConversationParticipant, { eq }) =>
eq(
userConversationParticipant.userId,
userConversationMessage.senderId
)
});
return result!;
}
}),
content: t.exposeString('content', { nullable: false }),
createdAt: t.expose('createdAt', { type: 'Date', nullable: false })
})
});
40 changes: 40 additions & 0 deletions src/types/user/conversation/Participant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { User, UserConversation } from '../..';
import { builder } from '../../../builder';
import { db } from '../../../drizzle/db';
import { type UserConversationParticipantSchemaType } from '../../../drizzle/schema';

export const UserConversationParticipant =
builder.objectRef<UserConversationParticipantSchemaType>(
'UserConversationParticipant'
);

UserConversationParticipant.implement({
fields: (t) => ({
user: t.field({
type: User,
nullable: false,
resolve: async (userConversationParticipant) => {
const result = await db.query.user.findFirst({
where: (user, { eq }) =>
eq(user.id, userConversationParticipant.userId)
});
return result!;
}
}),
conversation: t.field({
type: UserConversation,
nullable: false,
resolve: async (userConversationParticipant) => {
const result = await db.query.userConversation.findFirst({
where: (userConversation, { eq }) =>
eq(
userConversation.id,
userConversationParticipant.conversationId
)
});
return result!;
}
}),
joinedAt: t.expose('joinedAt', { type: 'Date', nullable: false })
})
});
1 change: 1 addition & 0 deletions src/types/user/conversation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Conversation';
1 change: 1 addition & 0 deletions src/types/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './conversation';
export * from './ProfileField';
export * from './Relationship';
export * from './User';
Expand Down

0 comments on commit f79588b

Please sign in to comment.