Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions apps/api/scripts/seed-dms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* Seed DM conversations for a user.
*
* Usage:
* pnpm --filter @repo/api exec tsx scripts/seed-dms.ts <user-id>
*/

import { db } from "@repo/db"
import { channel, channelMember, message, user } from "@repo/db/schema"
import { eq, inArray } from "drizzle-orm"

const userId = process.argv[2]
if (!userId) {
console.error(
"Usage: pnpm --filter @repo/api exec tsx scripts/seed-dms.ts <user-id>"
)
process.exit(1)
}

const fakeUsers = [
{ name: "Alice Chen", username: "alice", email: "alice@fake.local" },
{ name: "Bob Martinez", username: "bobm", email: "bob@fake.local" },
{ name: "Charlie Kim", username: "charliek", email: "charlie@fake.local" },
{ name: "Dana Patel", username: "danap", email: "dana@fake.local" },
{ name: "Eli Thompson", username: "elit", email: "eli@fake.local" },
{ name: "Fay Nakamura", username: "fayn", email: "fay@fake.local" },
{ name: "Gus Rivera", username: "gusr", email: "gus@fake.local" },
{ name: "Hana Okonkwo", username: "hanao", email: "hana@fake.local" },
]

const lastMessages = [
"Hey, are you free to chat?",
"Thanks for the help earlier!",
"Did you see the new update?",
"Let me know when you're online",
"lol that was hilarious",
"Sure, I'll send it over tomorrow",
"Can you review my PR when you get a chance?",
"GG, that was a close one",
]

const groupDms = [
{
name: "Weekend Plans",
members: ["alice", "bobm", "charliek"],
messages: [
{ from: "alice", content: "Anyone free Saturday?" },
{ from: "bobm", content: "I'm down, what are you thinking?" },
{ from: "charliek", content: "Same, let me know the plan" },
],
},
{
name: "Dev Team",
members: ["danap", "elit", "fayn", "gusr"],
messages: [
{ from: "gusr", content: "standup in 5" },
{ from: "elit", content: "be there" },
{ from: "fayn", content: "omw" },
{ from: "danap", content: "👍" },
],
},
{
name: "Book Club",
members: ["hanao", "alice", "danap"],
messages: [
{ from: "hanao", content: "finished chapter 12, no spoilers please!" },
{ from: "alice", content: "same, that ending tho" },
{ from: "danap", content: "meeting Thursday still?" },
],
},
{
name: "Game Night",
members: ["bobm", "charliek", "elit", "gusr", "fayn"],
messages: [
{ from: "charliek", content: "who's playing tonight?" },
{ from: "bobm", content: "in" },
{ from: "elit", content: "🎮 let's go" },
{ from: "gusr", content: "starting at 9?" },
{ from: "fayn", content: "works for me" },
],
},
]

async function seed() {
// Verify the target user exists
const [targetUser] = await db.select().from(user).where(eq(user.id, userId))
if (!targetUser) {
console.error(`User ${userId} not found`)
process.exit(1)
}
console.log(`Seeding DMs for ${targetUser.name} (${targetUser.email})\n`)

// Clean up previous seed data
const existingFake = await db
.select({ id: user.id })
.from(user)
.where(
inArray(
user.email,
fakeUsers.map((u) => u.email)
)
)
if (existingFake.length > 0) {
const fakeIds = existingFake.map((u) => u.id)
// Deleting users cascades to channel_member and messages
await db.delete(user).where(inArray(user.id, fakeIds))
// Clean up orphaned DM channels with no members
const orphaned = await db
.select({ id: channel.id })
.from(channel)
.where(eq(channel.type, "dm"))
for (const ch of orphaned) {
const members = await db
.select({ id: channelMember.id })
.from(channelMember)
.where(eq(channelMember.channelId, ch.id))
if (members.length === 0) {
await db.delete(channel).where(eq(channel.id, ch.id))
}
}
console.log("Cleared previous seed data")
Comment thread
BuckyMcYolo marked this conversation as resolved.
}

// Create fake users and DM channels
for (let i = 0; i < fakeUsers.length; i++) {
const fake = fakeUsers[i]

const [fakeUser] = await db
.insert(user)
.values({
name: fake.name,
email: fake.email,
username: fake.username,
displayUsername: fake.username,
emailVerified: true,
})
.returning()

// Create DM channel
const [dmChannel] = await db
.insert(channel)
.values({
type: "dm",
guildId: null,
position: 0,
})
.returning()

// Add both users as members
await db.insert(channelMember).values([
{ channelId: dmChannel.id, userId },
{ channelId: dmChannel.id, userId: fakeUser.id },
])

// Add a last message from the fake user
const minutesAgo = (fakeUsers.length - i) * 15
const createdAt = new Date(Date.now() - minutesAgo * 60 * 1000)
await db.insert(message).values({
channelId: dmChannel.id,
authorId: fakeUser.id,
content: lastMessages[i],
type: "default",
createdAt,
})

// Update channel updatedAt to match message time for correct sort order
await db
.update(channel)
.set({ updatedAt: createdAt })
.where(eq(channel.id, dmChannel.id))

console.log(` ${fake.name} (@${fake.username}): "${lastMessages[i]}"`)
}

console.log(`\nCreated ${fakeUsers.length} DM conversations`)

// Build a map of username → user id for group DM membership
const seededUsers = await db
.select({ id: user.id, username: user.username })
.from(user)
.where(
inArray(
user.username,
fakeUsers.map((u) => u.username)
)
)
const usernameToId = Object.fromEntries(
seededUsers.map((u) => [u.username, u.id])
)

// Create group DMs
for (let i = 0; i < groupDms.length; i++) {
const group = groupDms[i]
const minutesAgo = (groupDms.length - i) * 30 + fakeUsers.length * 15

const [dmChannel] = await db
.insert(channel)
.values({
type: "group_dm",
name: group.name,
guildId: null,
ownerId: userId,
position: 0,
})
.returning()

// Add the target user + all named members
const memberIds = [
userId,
...group.members
.map((username) => usernameToId[username])
.filter(Boolean),
]
await db.insert(channelMember).values(
memberIds.map((memberId) => ({
channelId: dmChannel.id,
userId: memberId,
}))
)

// Insert messages with staggered timestamps
for (let j = 0; j < group.messages.length; j++) {
const msg = group.messages[j]
const authorId = usernameToId[msg.from]
if (!authorId) continue
const msgTime = new Date(
Date.now() - minutesAgo * 60 * 1000 + j * 2 * 60 * 1000
)
await db.insert(message).values({
channelId: dmChannel.id,
authorId,
content: msg.content,
type: "default",
createdAt: msgTime,
})
}

// Set channel updatedAt to last message time
const lastMsgTime = new Date(
Date.now() -
minutesAgo * 60 * 1000 +
(group.messages.length - 1) * 2 * 60 * 1000
)
await db
.update(channel)
.set({ updatedAt: lastMsgTime })
.where(eq(channel.id, dmChannel.id))

console.log(
` Group "${group.name}" (${memberIds.length} members): "${group.messages[group.messages.length - 1].content}"`
)
}

console.log(`\nCreated ${groupDms.length} group DM conversations`)
process.exit(0)
}

seed()
6 changes: 5 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import createApp from "@/lib/helpers/app/create-app"
import configureOpenAPI from "@/lib/helpers/openapi/configure-openapi"
import index from "@/routes/index.route"
import channelsRouter from "@/routes/v1/channels/index"
import dmsRouter from "@/routes/v1/dms/index"
import waitlistRouter from "@/routes/waitlist/index"

const app = createApp()
Expand All @@ -28,7 +29,10 @@ configureOpenAPI(app)
app.route("/", index)

// Route mounting — chained for Hono RPC type inference
const routes = app.route("/", waitlistRouter).route("/v1", channelsRouter)
const routes = app
.route("/", waitlistRouter)
.route("/v1", channelsRouter)
.route("/v1", dmsRouter)

export type AppType = typeof routes

Expand Down
17 changes: 17 additions & 0 deletions apps/api/src/lib/helpers/openapi/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { z } from "@hono/zod-openapi"
import type { ZodType } from "zod"
import jsonContent from "./json-content"

// ── Pagination ──────────────────────────────────────────

export const paginationQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
perPage: z.coerce.number().int().min(1).max(100).default(50),
})

export const paginatedResponseSchema = <T extends ZodType>(itemSchema: T) =>
z.object({
itemsTotal: z.number(),
currentPage: z.number(),
nextPage: z.number().nullable(),
prevPage: z.number().nullable(),
data: z.array(itemSchema),
})

const errorSchema = z.object({
success: z.literal(false),
message: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import type { AppBindings } from "@/lib/types/app-types"

/**
* Authenticates the request via better-auth session and resolves the
* user's active guild + membership.
* guild from the :guildSlug path parameter. Verifies the user is a
* member of the guild.
*
* Sets in context:
* - user: The authenticated user
* - session: The session object
* - guild: The user's active guild
* - member: The user's membership in the active guild
* - guild: The resolved guild
* - member: The user's membership in the guild
*/
export const authMiddleware = async (c: Context<AppBindings>, next: Next) => {
export const guildAuthMiddleware = async (
c: Context<AppBindings>,
next: Next
) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers })

if (!session) {
Expand All @@ -26,12 +30,19 @@ export const authMiddleware = async (c: Context<AppBindings>, next: Next) => {
)
}

const activeGuildId = session.session.activeOrganizationId
const guildSlug = c.req.param("guildSlug")

if (!activeGuildId) {
const guildRecord = await db
.select()
.from(guild)
.where(eq(guild.slug, guildSlug))
.limit(1)
.then((rows) => rows[0])

if (!guildRecord) {
return c.json(
{ success: false, message: "No active guild selected" },
HttpStatusCodes.BAD_REQUEST
{ success: false, message: "Guild not found" },
HttpStatusCodes.NOT_FOUND
)
}

Expand All @@ -41,33 +52,19 @@ export const authMiddleware = async (c: Context<AppBindings>, next: Next) => {
.where(
and(
eq(guildMember.userId, session.user.id),
eq(guildMember.guildId, activeGuildId)
eq(guildMember.guildId, guildRecord.id)
)
)
.limit(1)
.then((rows) => rows[0])

if (!memberRecord) {
return c.json(
{ success: false, message: "You are not a member of this guild" },
{ success: false, message: "Forbidden" },
HttpStatusCodes.FORBIDDEN
)
}

const guildRecord = await db
.select()
.from(guild)
.where(eq(guild.id, activeGuildId))
.limit(1)
.then((rows) => rows[0])

if (!guildRecord) {
return c.json(
{ success: false, message: "Guild not found" },
HttpStatusCodes.NOT_FOUND
)
}

c.set("user", session.user)
c.set("session", session.session)
c.set("guild", guildRecord)
Expand Down
Loading