Merge branch 'dev-1' into dev

This commit is contained in:
Wuesteon 2025-12-05 17:57:26 +01:00
commit d41d060bb3
1770 changed files with 168028 additions and 31031 deletions

View file

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './db/database.module';
import { HealthModule } from './health/health.module';
import { MoodsModule } from './moods/moods.module';
import { SequencesModule } from './sequences/sequences.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
HealthModule,
MoodsModule,
SequencesModule,
],
})
export class AppModule {}

View file

@ -0,0 +1,38 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import * as schema from './schema';
// Use require for postgres to avoid ESM/CommonJS interop issues
const postgres = require('postgres');
let connection: ReturnType<typeof postgres> | null = null;
let db: ReturnType<typeof drizzle> | null = null;
export function getConnection(databaseUrl: string) {
if (!connection) {
connection = postgres(databaseUrl, {
max: 10,
idle_timeout: 20,
connect_timeout: 10,
});
}
return connection;
}
export function getDb(databaseUrl: string) {
if (!db) {
const conn = getConnection(databaseUrl);
db = drizzle(conn, { schema });
}
return db;
}
export async function closeConnection() {
if (connection) {
await connection.end();
connection = null;
db = null;
}
}
export type Database = ReturnType<typeof getDb>;

View file

@ -0,0 +1,29 @@
import { Module, Global, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getDb, closeConnection } from './connection';
import type { Database } from './connection';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService): Database => {
const databaseUrl = configService.get<string>('DATABASE_URL');
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
return getDb(databaseUrl);
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule implements OnModuleDestroy {
async onModuleDestroy() {
await closeConnection();
}
}

View file

@ -0,0 +1,2 @@
export * from './moods.schema';
export * from './sequences.schema';

View file

@ -0,0 +1,15 @@
import { pgTable, uuid, text, jsonb, boolean, timestamp } from 'drizzle-orm/pg-core';
export const moods = pgTable('moods', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').notNull(),
name: text('name').notNull(),
colors: jsonb('colors').notNull().$type<string[]>(),
animation: text('animation'),
isDefault: boolean('is_default').default(false),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export type Mood = typeof moods.$inferSelect;
export type NewMood = typeof moods.$inferInsert;

View file

@ -0,0 +1,14 @@
import { pgTable, uuid, text, jsonb, integer, timestamp } from 'drizzle-orm/pg-core';
export const sequences = pgTable('sequences', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').notNull(),
name: text('name').notNull(),
moodIds: jsonb('mood_ids').notNull().$type<string[]>(),
duration: integer('duration').default(30),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export type Sequence = typeof sequences.$inferSelect;
export type NewSequence = typeof sequences.$inferInsert;

View file

@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
check() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
service: 'moods-backend',
};
}
}

View file

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View file

@ -0,0 +1,40 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable CORS for mobile and web apps
const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [
'http://localhost:3000',
'http://localhost:5173',
'http://localhost:5182',
'http://localhost:8081',
'exp://localhost:8081',
'http://localhost:3001',
];
app.enableCors({
origin: corsOrigins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
credentials: true,
});
// Enable validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
})
);
// Set global prefix for API routes
app.setGlobalPrefix('api/v1');
const port = process.env.PORT || 3012;
await app.listen(port);
console.log(`Moods backend running on http://localhost:${port}`);
}
bootstrap();

View file

@ -0,0 +1,37 @@
import { IsString, IsArray, IsBoolean, IsOptional } from 'class-validator';
export class CreateMoodDto {
@IsString()
name: string;
@IsArray()
@IsString({ each: true })
colors: string[];
@IsString()
@IsOptional()
animation?: string;
@IsBoolean()
@IsOptional()
isDefault?: boolean;
}
export class UpdateMoodDto {
@IsString()
@IsOptional()
name?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
colors?: string[];
@IsString()
@IsOptional()
animation?: string;
@IsBoolean()
@IsOptional()
isDefault?: boolean;
}

View file

@ -0,0 +1,40 @@
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { MoodsService } from './moods.service';
import { CreateMoodDto, UpdateMoodDto } from './dto';
@Controller('moods')
@UseGuards(JwtAuthGuard)
export class MoodsController {
constructor(private readonly moodsService: MoodsService) {}
@Get()
async findAll(@CurrentUser() user: CurrentUserData) {
return this.moodsService.findAllByUser(user.userId);
}
@Get(':id')
async findOne(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
return this.moodsService.findOne(id, user.userId);
}
@Post()
async create(@Body() dto: CreateMoodDto, @CurrentUser() user: CurrentUserData) {
return this.moodsService.create(user.userId, dto);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateMoodDto,
@CurrentUser() user: CurrentUserData
) {
return this.moodsService.update(id, user.userId, dto);
}
@Delete(':id')
async delete(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
await this.moodsService.delete(id, user.userId);
return { success: true };
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MoodsController } from './moods.controller';
import { MoodsService } from './moods.service';
@Module({
controllers: [MoodsController],
providers: [MoodsService],
exports: [MoodsService],
})
export class MoodsModule {}

View file

@ -0,0 +1,64 @@
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { type Database } from '../db/connection';
import { moods, type Mood, type NewMood } from '../db/schema/moods.schema';
import { CreateMoodDto, UpdateMoodDto } from './dto';
@Injectable()
export class MoodsService {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async findAllByUser(userId: string): Promise<Mood[]> {
return this.db.select().from(moods).where(eq(moods.userId, userId));
}
async findOne(id: string, userId: string): Promise<Mood> {
const [mood] = await this.db
.select()
.from(moods)
.where(and(eq(moods.id, id), eq(moods.userId, userId)));
if (!mood) {
throw new NotFoundException(`Mood with ID ${id} not found`);
}
return mood;
}
async create(userId: string, dto: CreateMoodDto): Promise<Mood> {
const newMood: NewMood = {
userId,
name: dto.name,
colors: dto.colors,
animation: dto.animation,
isDefault: dto.isDefault ?? false,
};
const [mood] = await this.db.insert(moods).values(newMood).returning();
return mood;
}
async update(id: string, userId: string, dto: UpdateMoodDto): Promise<Mood> {
// Verify the mood exists and belongs to the user
await this.findOne(id, userId);
const [updated] = await this.db
.update(moods)
.set({
...dto,
updatedAt: new Date(),
})
.where(and(eq(moods.id, id), eq(moods.userId, userId)))
.returning();
return updated;
}
async delete(id: string, userId: string): Promise<void> {
// Verify the mood exists and belongs to the user
await this.findOne(id, userId);
await this.db.delete(moods).where(and(eq(moods.id, id), eq(moods.userId, userId)));
}
}

View file

@ -0,0 +1,29 @@
import { IsString, IsArray, IsNumber, IsOptional } from 'class-validator';
export class CreateSequenceDto {
@IsString()
name: string;
@IsArray()
@IsString({ each: true })
moodIds: string[];
@IsNumber()
@IsOptional()
duration?: number;
}
export class UpdateSequenceDto {
@IsString()
@IsOptional()
name?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
moodIds?: string[];
@IsNumber()
@IsOptional()
duration?: number;
}

View file

@ -0,0 +1,40 @@
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
import { SequencesService } from './sequences.service';
import { CreateSequenceDto, UpdateSequenceDto } from './dto';
@Controller('sequences')
@UseGuards(JwtAuthGuard)
export class SequencesController {
constructor(private readonly sequencesService: SequencesService) {}
@Get()
async findAll(@CurrentUser() user: CurrentUserData) {
return this.sequencesService.findAllByUser(user.userId);
}
@Get(':id')
async findOne(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
return this.sequencesService.findOne(id, user.userId);
}
@Post()
async create(@Body() dto: CreateSequenceDto, @CurrentUser() user: CurrentUserData) {
return this.sequencesService.create(user.userId, dto);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateSequenceDto,
@CurrentUser() user: CurrentUserData
) {
return this.sequencesService.update(id, user.userId, dto);
}
@Delete(':id')
async delete(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
await this.sequencesService.delete(id, user.userId);
return { success: true };
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SequencesController } from './sequences.controller';
import { SequencesService } from './sequences.service';
@Module({
controllers: [SequencesController],
providers: [SequencesService],
exports: [SequencesService],
})
export class SequencesModule {}

View file

@ -0,0 +1,63 @@
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { type Database } from '../db/connection';
import { sequences, type Sequence, type NewSequence } from '../db/schema/sequences.schema';
import { CreateSequenceDto, UpdateSequenceDto } from './dto';
@Injectable()
export class SequencesService {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async findAllByUser(userId: string): Promise<Sequence[]> {
return this.db.select().from(sequences).where(eq(sequences.userId, userId));
}
async findOne(id: string, userId: string): Promise<Sequence> {
const [sequence] = await this.db
.select()
.from(sequences)
.where(and(eq(sequences.id, id), eq(sequences.userId, userId)));
if (!sequence) {
throw new NotFoundException(`Sequence with ID ${id} not found`);
}
return sequence;
}
async create(userId: string, dto: CreateSequenceDto): Promise<Sequence> {
const newSequence: NewSequence = {
userId,
name: dto.name,
moodIds: dto.moodIds,
duration: dto.duration ?? 30,
};
const [sequence] = await this.db.insert(sequences).values(newSequence).returning();
return sequence;
}
async update(id: string, userId: string, dto: UpdateSequenceDto): Promise<Sequence> {
// Verify the sequence exists and belongs to the user
await this.findOne(id, userId);
const [updated] = await this.db
.update(sequences)
.set({
...dto,
updatedAt: new Date(),
})
.where(and(eq(sequences.id, id), eq(sequences.userId, userId)))
.returning();
return updated;
}
async delete(id: string, userId: string): Promise<void> {
// Verify the sequence exists and belongs to the user
await this.findOne(id, userId);
await this.db.delete(sequences).where(and(eq(sequences.id, id), eq(sequences.userId, userId)));
}
}