v1: delete old repo, initalize monorepo structure, config, database, and api packages basic setup

This commit is contained in:
2025-10-10 21:17:32 -04:00
parent ccceb77179
commit e7fff00001
75 changed files with 1362 additions and 1432 deletions

38
packages/database/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
# prisma recommended
generated

View File

@@ -0,0 +1,28 @@
{
"name": "@concord/database",
"scripts": {
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:seed": "bun src/seed.ts",
"lint": "biome lint .",
"format": "biome format --write .",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@concord/biome-config": "workspace:*",
"@concord/tsconfig": "workspace:*",
"@types/bun": "latest",
"bcryptjs": "^3.0.2",
"prisma": "^6.17.0"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@prisma/client": "^6.17.0"
},
"exports": {
".": "./src/client.ts"
}
}

View File

@@ -0,0 +1,234 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client"
output = "../generated"
}
// Enums
enum UserStatus {
online
offline
invisible
dnd
idle
}
enum ChannelType {
voice
text
forum
}
// Core
model User {
id String @id @default(cuid())
username String @unique @db.VarChar(32)
nickname String? @db.VarChar(64)
bio String? @db.VarChar(1024)
picture String? @db.VarChar(2048)
banner String? @db.VarChar(2048)
status UserStatus @default(offline)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
passwordHash String @db.VarChar(255)
isOwner Boolean @default(false)
isAdmin Boolean @default(false)
// Relations
RoleAssign RoleAssign[]
Message Message[]
Reaction Reaction[]
UserPing UserPing[]
Timeout Timeout[]
RefreshToken RefreshToken[]
}
model Server {
id String @id @default(cuid())
name String @unique @db.VarChar(32)
description String? @db.VarChar(1024)
icon String? @db.VarChar(2048)
// FKs
headCategoryId String? @unique @map("head_category_id")
headCategory Category? @relation("ServerHeadCategory", fields: [headCategoryId], references: [id], onUpdate: NoAction, onDelete: SetNull)
// Relations
Category Category[]
Channel Channel[]
Role Role[]
Timeout Timeout[]
}
model Category {
id String @id @default(cuid())
name String @unique @db.VarChar(64)
// FKs (Linked-List)
nextCategoryId String? @unique @map("next_category_id")
nextCategory Category? @relation("CategoryOrder", fields: [nextCategoryId], references: [id], onUpdate: NoAction, onDelete: SetNull)
prevCategory Category? @relation("CategoryOrder")
serverId String @map("server_id")
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
headChannelId String? @unique @map("head_channel_id")
headChannel Channel? @relation("CategoryHeadChannel", fields: [headChannelId], references: [id], onUpdate: NoAction, onDelete: SetNull)
// Relations
serverLink Server? @relation("ServerHeadCategory")
}
model Channel {
id String @id @default(cuid())
name String @unique @db.VarChar(64)
description String? @db.VarChar(1024)
type ChannelType @default(text)
// FKs (Linked-List)
nextChannelId String? @unique @map("next_channel_id")
nextChannel Channel? @relation("ChannelOrder", fields: [nextChannelId], references: [id], onUpdate: NoAction, onDelete: SetNull)
prevChannel Channel? @relation("ChannelOrder")
serverId String @map("server_id")
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
// Relations
categoryLink Category? @relation("CategoryHeadChannel")
Message Message[]
ChannelPermissions ChannelPermissions[]
}
model Message {
id String @id @default(cuid())
text String @db.VarChar(8096)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
deletedAt DateTime?
// FKs
channelId String @map("channel_id")
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Replies
repliesToId String? @map("replies_to_id")
repliesTo Message? @relation("Replies", fields: [repliesToId], references: [id], onUpdate: NoAction, onDelete: SetNull)
replies Message[] @relation("Replies")
// Relations
Reaction Reaction[]
UserPing UserPing[]
RolePing RolePing[]
Attachment Attachment[]
}
model Role {
id String @id @default(cuid())
name String @unique @db.VarChar(64)
canModifyRoles Boolean @default(false)
canCreateChannels Boolean @default(false)
canKickUsers Boolean @default(false)
canTimeout Boolean @default(false)
// FKs
serverId String @map("server_id")
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
// Relations
RoleAssign RoleAssign[]
ChannelPermissions ChannelPermissions[]
RolePing RolePing[]
}
// Supporting
model RoleAssign {
// Foreign Keys as composite primary key
userId String @map("user_id")
roleId String @map("role_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([userId, roleId])
}
model ChannelPermissions {
id String @id @default(cuid())
channelId String @map("channel_id")
roleId String @map("role_id")
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
}
model Reaction {
id String @id @default(cuid())
emoji String
messageId String @map("message_id")
userId String @map("user_id")
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([messageId, userId, emoji])
}
model UserPing {
// Foreign Keys as composite primary key
userId String @map("user_id")
messageId String @map("message_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
@@id([userId, messageId])
}
model RolePing {
// FKs as composite primary key
roleId String @map("role_id")
messageId String @map("message_id")
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
@@id([roleId, messageId])
}
model Attachment {
id String @id @default(cuid())
url String @db.VarChar(2000)
fileName String @map("file_name") @db.VarChar(255)
mimeType String @map("mime_type") @db.VarChar(64)
size Int
// FKs
messageId String @map("message_id")
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
}
model Timeout {
timeoutUntil DateTime @map("timeout_until")
description String? @db.VarChar(1000)
// FKs
userId String @map("user_id")
serverId String @map("server_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
@@id([userId, serverId])
}
model RefreshToken {
id String @id @default(cuid())
tokenHash String @unique @db.VarChar(255)
createdAt DateTime @default(now())
expiresAt DateTime
device String? @db.VarChar(255)
// FKs
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

View File

@@ -0,0 +1,15 @@
import { PrismaClient } from '../generated/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma
|| new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
export * from '../generated/client';

View File

View File

@@ -0,0 +1,163 @@
import * as bcrypt from 'bcryptjs';
import { PrismaClient } from './client';
const prisma = new PrismaClient();
async function main() {
console.log('Start seeding...');
const hashedPassword = await bcrypt.hash('test', 10);
console.log('Generated password hash.');
// Clean up existing data to ensure a fresh start
await prisma.roleAssign.deleteMany();
await prisma.message.deleteMany();
await prisma.channel.deleteMany();
await prisma.category.deleteMany();
await prisma.role.deleteMany();
await prisma.server.deleteMany();
await prisma.user.deleteMany();
console.log('Cleaned up old data.');
// Create Users
const alice = await prisma.user.create({
data: {
username: 'Alice',
nickname: 'Alice',
passwordHash: hashedPassword,
isOwner: true,
},
});
const bob = await prisma.user.create({
data: {
username: 'Bob',
nickname: 'Bobby',
passwordHash: hashedPassword,
isAdmin: true,
},
});
const charlie = await prisma.user.create({
data: {
username: 'Charlie',
passwordHash: hashedPassword,
},
});
console.log(`Created users: ${alice.username}, ${bob.username}, ${charlie.username}`);
// Create a Server
const server = await prisma.server.create({
data: {
name: 'TS Lovers',
description: 'Static Types Win',
},
});
console.log(`Created server: ${server.name}`);
// Create Roles for the server
const adminRole = await prisma.role.create({
data: {
name: 'Admin',
serverId: server.id,
canCreateChannels: true,
canKickUsers: true,
canModifyRoles: true,
canTimeout: true,
},
});
const memberRole = await prisma.role.create({
data: { name: 'Member', serverId: server.id },
});
console.log(`Created roles: ${adminRole.name}, ${memberRole.name}`);
// Assign roles to users
await prisma.roleAssign.createMany({
data: [
{ userId: alice.id, roleId: adminRole.id },
{ userId: bob.id, roleId: adminRole.id },
{ userId: charlie.id, roleId: memberRole.id },
],
});
console.log('Assigned roles to users.');
// Create Categories
const generalCategory = await prisma.category.create({
data: {
name: 'General',
serverId: server.id,
},
});
const devCategory = await prisma.category.create({
data: {
name: 'Development',
serverId: server.id,
},
});
// Set the category order (linked-list) and head category for the server
await prisma.server.update({
where: { id: server.id },
data: { headCategoryId: generalCategory.id },
});
await prisma.category.update({
where: { id: generalCategory.id },
data: { nextCategoryId: devCategory.id },
});
console.log(`Created categories: ${generalCategory.name}, ${devCategory.name}`);
// Create Channels
const welcomeChannel = await prisma.channel.create({
data: {
name: 'welcome',
serverId: server.id,
},
});
const announcementsChannel = await prisma.channel.create({
data: {
name: 'announcements',
serverId: server.id,
},
});
// Set channel order for the "General" category
await prisma.category.update({
where: { id: generalCategory.id },
data: { headChannelId: welcomeChannel.id },
});
await prisma.channel.update({
where: { id: welcomeChannel.id },
data: { nextChannelId: announcementsChannel.id },
});
console.log(`Created channels: ${welcomeChannel.name}, ${announcementsChannel.name}`);
// Create a Message
const firstMessage = await prisma.message.create({
data: {
text: `Welcome to the ${server.name} server, ${bob.username}!`,
channelId: welcomeChannel.id,
userId: alice.id,
},
});
console.log(`Created a welcome message.`);
// Create a Reply
await prisma.message.create({
data: {
text: 'Glad to be here!',
channelId: welcomeChannel.id,
userId: bob.id,
repliesToId: firstMessage.id,
},
});
console.log(`Created a reply.`);
console.log('Seeding finished.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,5 @@
{
"extends": "@concord/tsconfig/base.json",
"include": ["."]
"exclude": ["generated"]
}