Compare commits

...

3 Commits

43 changed files with 1645 additions and 56 deletions

View File

@@ -1,6 +1,6 @@
# home-service
Современный монорепозиторий для сервиса домашней автоматизации с бэкендом и админ-панелью.
Современный монорепозиторий для сервиса домашней автоматизации с бэкендом и фронтенд-панелью.
## 🚀 Стек технологий
@@ -15,7 +15,7 @@
- Zod для валидации
- date-fns для работы с датами
**Admin:**
**Frontend:**
- Next.js 16 (App Router)
- React 19
- TailwindCSS 4
@@ -34,7 +34,7 @@ home-service/
│ ├── migrations/ # Миграции БД
│ └── data/ # База данных PGLite
├── admin/ # Next.js админка (@home-service/admin)
├── frontend/ # Next.js админка (@home-service/frontend)
│ └── src/
│ ├── app/ # Страницы
│ ├── components/ # React компоненты
@@ -86,13 +86,13 @@ pnpm dev
# Запустить только backend
pnpm --filter @home-service/backend dev
# Запустить только admin
pnpm --filter @home-service/admin dev
# Запустить только frontend
pnpm --filter @home-service/frontend dev
```
**Адреса:**
- Backend API: `http://localhost:3000`
- Admin панель: `http://localhost:3001`
- Frontend панель: `http://localhost:3001`
### Сборка
@@ -103,8 +103,8 @@ pnpm build
# Собрать только backend
pnpm --filter @home-service/backend build
# Собрать только admin
pnpm --filter @home-service/admin build
# Собрать только frontend
pnpm --filter @home-service/frontend build
```
### Другие команды
@@ -183,7 +183,7 @@ CORS_ORIGIN=http://localhost:3001
DATABASE_PATH=./data/events.db
```
### Admin (.env.local)
### Frontend (.env.local)
```
NEXT_PUBLIC_API_URL=http://localhost:3000
```
@@ -205,7 +205,7 @@ curl -X POST http://localhost:3000/api/events \
```bash
pnpm build
pnpm --filter @home-service/backend start:prod
pnpm --filter @home-service/admin start
pnpm --filter @home-service/frontend start
```
### Backend отдельно
@@ -214,10 +214,10 @@ pnpm --filter @home-service/backend build
pnpm --filter @home-service/backend start:prod
```
### Admin отдельно
### Frontend отдельно
```bash
pnpm --filter @home-service/admin build
pnpm --filter @home-service/admin start
pnpm --filter @home-service/frontend build
pnpm --filter @home-service/frontend start
```
## 🛠️ Разработка

View File

@@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

3
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
uploads/*
!uploads/.gitkeep

View File

@@ -0,0 +1,29 @@
CREATE TABLE "wishlist_categories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"min_price" integer DEFAULT 0 NOT NULL,
"max_price" integer,
"color" text,
"icon" text,
"order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "wishlist_categories_name_unique" UNIQUE("name"),
CONSTRAINT "wishlist_categories_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "wishlist_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"description" text,
"price" integer NOT NULL,
"currency" text DEFAULT 'RUB' NOT NULL,
"link" text,
"images" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"category_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_category_id_wishlist_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."wishlist_categories"("id") ON DELETE restrict ON UPDATE no action;

View File

@@ -0,0 +1,311 @@
{
"id": "172d78f6-8d17-4bc0-882d-c85b984f0c22",
"prevId": "852b1a6a-31c7-4f44-bdef-a378a18e2c7d",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.events": {
"name": "events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"emoji": {
"name": "emoji",
"type": "text",
"primaryKey": false,
"notNull": true
},
"month": {
"name": "month",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"day": {
"name": "day",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"start_year": {
"name": "start_year",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"end_month": {
"name": "end_month",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"end_day": {
"name": "end_day",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"end_year": {
"name": "end_year",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"tags": {
"name": "tags",
"type": "text[]",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.wishlist_categories": {
"name": "wishlist_categories",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"min_price": {
"name": "min_price",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"max_price": {
"name": "max_price",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false
},
"icon": {
"name": "icon",
"type": "text",
"primaryKey": false,
"notNull": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"wishlist_categories_name_unique": {
"name": "wishlist_categories_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
},
"wishlist_categories_slug_unique": {
"name": "wishlist_categories_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.wishlist_items": {
"name": "wishlist_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"currency": {
"name": "currency",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'RUB'"
},
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": false
},
"images": {
"name": "images",
"type": "text[]",
"primaryKey": false,
"notNull": true,
"default": "ARRAY[]::text[]"
},
"category_id": {
"name": "category_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"wishlist_items_category_id_wishlist_categories_id_fk": {
"name": "wishlist_items_category_id_wishlist_categories_id_fk",
"tableFrom": "wishlist_items",
"tableTo": "wishlist_categories",
"columnsFrom": [
"category_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1764421778123,
"tag": "0000_lyrical_gabe_jones",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1765007898910,
"tag": "0001_dashing_molten_man",
"breakpoints": true
}
]
}

View File

@@ -25,7 +25,8 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-express": "^11.1.9",
"@nestjs/serve-static": "^5.0.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"date-fns": "^4.1.0",
@@ -41,6 +42,7 @@
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/multer": "^2.0.0",
"@types/node": "^22.19.1",
"@types/supertest": "^6.0.2",
"drizzle-kit": "^0.31.7",
@@ -51,6 +53,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3"
},
"jest": {

120
backend/seed.ts Normal file
View File

@@ -0,0 +1,120 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './src/app.module';
import { WishlistCategoriesService } from './src/wishlist/categories.service';
import { WishlistService } from './src/wishlist/wishlist.service';
async function seed() {
const app = await NestFactory.createApplicationContext(AppModule);
const categoriesService = app.get(WishlistCategoriesService);
const wishlistService = app.get(WishlistService);
console.log('🌱 Seeding wishlist categories...');
// Создать категории
const category1 = await categoriesService.create({
name: 'БЮДЖЕТНО',
slug: 'tier-1',
minPrice: 0,
maxPrice: 150000, // 1500 руб
color: '#00ff41',
icon: '🟢',
order: 1,
});
const category2 = await categoriesService.create({
name: 'СРЕДНИЙ',
slug: 'tier-2',
minPrice: 150001,
maxPrice: 500000, // 5000 руб
color: '#00cc33',
icon: '🟡',
order: 2,
});
const category3 = await categoriesService.create({
name: 'ТОП',
slug: 'tier-3',
minPrice: 500001,
maxPrice: null, // без ограничения
color: '#009922',
icon: '🔴',
order: 3,
});
console.log(`✅ Created 3 categories`);
console.log('🌱 Seeding wishlist items...');
// Создать примерные товары
await wishlistService.create({
title: 'Зерновой Кофе (Эфиопия)',
description: 'Люблю светлую обжарку. Желательно Эфиопия или Кения. Нужен именно в зернах.',
price: 85000, // 850 руб
currency: 'RUB',
link: 'https://ozon.ru',
imageUrls: ['https://images.unsplash.com/photo-1497935586351-b67a49e012bf?w=500'],
categoryId: category1.id,
});
await wishlistService.create({
title: 'Молескин в точку',
description: 'Черный, классический. Обязательно в точку, а не в линейку.',
price: 120000, // 1200 руб
currency: 'RUB',
link: 'https://wildberries.ru',
imageUrls: ['https://images.unsplash.com/photo-1531346878377-a513bc957374?w=500'],
categoryId: category1.id,
});
await wishlistService.create({
title: 'Винил: Daft Punk',
description: 'Альбом "Random Access Memories". Мечтаю послушать его на проигрывателе.',
price: 350000, // 3500 руб
currency: 'RUB',
link: 'https://market.yandex.ru',
imageUrls: ['https://images.unsplash.com/photo-1603048588665-791ca8aea617?w=500'],
categoryId: category2.id,
});
await wishlistService.create({
title: 'D&D Стартовый набор',
description: '5-я редакция. Хочу попробовать поиграть с друзьями.',
price: 290000, // 2900 руб
currency: 'RUB',
link: 'https://hobbygames.ru',
imageUrls: ['https://images.unsplash.com/photo-1632501641765-e568d90e09b2?w=500'],
categoryId: category2.id,
});
await wishlistService.create({
title: 'LEGO Speed Champions',
description: 'Любая машинка из этой серии, желательно Porsche или Ferrari.',
price: 250000, // 2500 руб
currency: 'RUB',
link: 'https://detmir.ru',
imageUrls: ['https://images.unsplash.com/photo-1585366119957-e9730b6d0f60?w=500'],
categoryId: category2.id,
});
await wishlistService.create({
title: 'Keychron K2',
description: 'Механическая клавиатура. Свичи Red или Brown. Нужна подсветка.',
price: 900000, // 9000 руб
currency: 'RUB',
link: 'https://geekboards.ru',
imageUrls: ['https://images.unsplash.com/photo-1595225476474-87563907a212?w=500'],
categoryId: category3.id,
});
console.log(`✅ Created 6 wishlist items`);
console.log('✨ Seeding completed!');
await app.close();
process.exit(0);
}
seed().catch((error) => {
console.error('❌ Seeding failed:', error);
process.exit(1);
});

View File

@@ -1,15 +1,24 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { DatabaseModule } from './database/database.module';
import { EventsModule } from './events/events.module';
import { WishlistModule } from './wishlist/wishlist.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'uploads'),
serveRoot: '/uploads',
}),
DatabaseModule,
EventsModule,
WishlistModule,
],
})
export class AppModule {}
export class AppModule { }

View File

@@ -22,7 +22,7 @@ const databaseProvider = {
const client = new PGlite(dbPath);
const db = drizzle(client, { schema });
// Создаем таблицу напрямую вместо использования миграций
// Создаем таблицы напрямую вместо использования миграций
try {
await client.exec(`
CREATE TABLE IF NOT EXISTS events (
@@ -42,6 +42,32 @@ const databaseProvider = {
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS wishlist_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
slug TEXT NOT NULL UNIQUE,
min_price INTEGER NOT NULL DEFAULT 0,
max_price INTEGER,
color TEXT,
icon TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS wishlist_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
description TEXT,
price INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'RUB',
link TEXT,
images TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
category_id UUID NOT NULL REFERENCES wishlist_categories(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
`);
console.log('✅ Database initialized successfully');
} catch (error) {
@@ -58,4 +84,4 @@ const databaseProvider = {
providers: [databaseProvider],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule {}
export class DatabaseModule { }

View File

@@ -37,3 +37,80 @@ export const events = pgTable('events', {
export type Event = typeof events.$inferSelect;
export type NewEvent = typeof events.$inferInsert;
// Wishlist Categories Table
export const wishlistCategories = pgTable('wishlist_categories', {
id: uuid('id')
.primaryKey()
.default(sql`gen_random_uuid()`),
// Основная информация
name: text('name').notNull().unique(), // "БЮДЖЕТНО", "СРЕДНИЙ", "ТОП"
slug: text('slug').notNull().unique(), // "tier-1", "tier-2", "tier-3"
// Диапазон цен (в копейках)
minPrice: integer('min_price').notNull().default(0),
maxPrice: integer('max_price'), // null = без ограничения
// Визуальные настройки
color: text('color'), // hex цвет для UI
icon: text('icon'), // emoji или класс иконки
order: integer('order').notNull().default(0), // порядок сортировки
// Служебные поля
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
});
export type WishlistCategory = typeof wishlistCategories.$inferSelect;
export type NewWishlistCategory = typeof wishlistCategories.$inferInsert;
// Wishlist Items Table
export const wishlistItems = pgTable('wishlist_items', {
id: uuid('id')
.primaryKey()
.default(sql`gen_random_uuid()`),
// Основная информация
title: text('title').notNull(),
description: text('description'),
price: integer('price').notNull(), // в копейках
currency: text('currency').notNull().default('RUB'),
// Ссылка на товар
link: text('link'),
// Множественные изображения (массив строк)
// Формат: ["url:https://...", "uploaded:/uploads/wishlist/file.jpg", ...]
images: text('images').array().notNull().default(sql`ARRAY[]::text[]`),
// Связь с категорией
categoryId: uuid('category_id')
.notNull()
.references(() => wishlistCategories.id, { onDelete: 'restrict' }),
// Служебные поля
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
});
export type WishlistItem = typeof wishlistItems.$inferSelect;
export type NewWishlistItem = typeof wishlistItems.$inferInsert;
// Helper type for image objects
export type WishlistImage = {
type: 'url' | 'uploaded';
path: string;
alt?: string;
};

View File

@@ -0,0 +1,41 @@
import { Controller, Get, Post, Patch, Delete, Body, Param, UsePipes } from '@nestjs/common';
import { WishlistCategoriesService } from './categories.service';
import type { CreateWishlistCategoryDto, UpdateWishlistCategoryDto } from './dto/category.dto';
import { ZodValidationPipe } from '../pipes/zod-validation.pipe';
import { CreateWishlistCategorySchema, UpdateWishlistCategorySchema } from './dto/category.dto';
@Controller('api/wishlist/categories')
export class WishlistCategoriesController {
constructor(private readonly categoriesService: WishlistCategoriesService) { }
@Get()
async findAll() {
return this.categoriesService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.categoriesService.findOne(id);
}
@Post()
@UsePipes(new ZodValidationPipe(CreateWishlistCategorySchema))
async create(@Body() dto: CreateWishlistCategoryDto) {
return this.categoriesService.create(dto);
}
@Patch(':id')
@UsePipes(new ZodValidationPipe(UpdateWishlistCategorySchema))
async update(
@Param('id') id: string,
@Body() dto: UpdateWishlistCategoryDto
) {
return this.categoriesService.update(id, dto);
}
@Delete(':id')
async delete(@Param('id') id: string) {
await this.categoriesService.delete(id);
return { message: 'Category deleted successfully' };
}
}

View File

@@ -0,0 +1,151 @@
import { Injectable, ConflictException, NotFoundException, Inject } from '@nestjs/common';
import { eq, and, lte, gte, or, isNull, asc } from 'drizzle-orm';
import { PgliteDatabase } from 'drizzle-orm/pglite';
import { DATABASE_CONNECTION } from '../database/database.module';
import * as schema from '../database/schema';
import { wishlistCategories, WishlistCategory, NewWishlistCategory } from '../database/schema';
import type { CreateWishlistCategoryDto, UpdateWishlistCategoryDto } from './dto/category.dto';
@Injectable()
export class WishlistCategoriesService {
constructor(
@Inject(DATABASE_CONNECTION)
private readonly db: PgliteDatabase<typeof schema>,
) { }
async findAll(): Promise<WishlistCategory[]> {
return this.db
.select()
.from(wishlistCategories)
.orderBy(asc(wishlistCategories.order));
}
async findOne(id: string): Promise<WishlistCategory> {
const [category] = await this.db
.select()
.from(wishlistCategories)
.where(eq(wishlistCategories.id, id))
.limit(1);
if (!category) {
throw new NotFoundException(`Category with ID ${id} not found`);
}
return category;
}
async findBySlug(slug: string): Promise<WishlistCategory | null> {
const [category] = await this.db
.select()
.from(wishlistCategories)
.where(eq(wishlistCategories.slug, slug))
.limit(1);
return category || null;
}
async create(dto: CreateWishlistCategoryDto): Promise<WishlistCategory> {
// Проверить уникальность slug
const existing = await this.findBySlug(dto.slug);
if (existing) {
throw new ConflictException(`Category with slug "${dto.slug}" already exists`);
}
// Проверить пересечение диапазонов цен
await this.validatePriceRange(dto.minPrice, dto.maxPrice);
const newCategory: NewWishlistCategory = {
...dto,
createdAt: new Date(),
updatedAt: new Date(),
};
const [category] = await this.db
.insert(wishlistCategories)
.values(newCategory)
.returning();
return category;
}
async update(id: string, dto: UpdateWishlistCategoryDto): Promise<WishlistCategory> {
const category = await this.findOne(id);
// Проверить уникальность slug если изменяется
if (dto.slug && dto.slug !== category.slug) {
const existing = await this.findBySlug(dto.slug);
if (existing) {
throw new ConflictException(`Category with slug "${dto.slug}" already exists`);
}
}
// Проверить пересечение диапазонов цен если изменяются
if (dto.minPrice !== undefined || dto.maxPrice !== undefined) {
const minPrice = dto.minPrice ?? category.minPrice;
const maxPrice = dto.maxPrice ?? category.maxPrice;
await this.validatePriceRange(minPrice, maxPrice, id);
}
const [updated] = await this.db
.update(wishlistCategories)
.set({ ...dto, updatedAt: new Date() })
.where(eq(wishlistCategories.id, id))
.returning();
return updated;
}
async delete(id: string): Promise<void> {
await this.findOne(id);
await this.db
.delete(wishlistCategories)
.where(eq(wishlistCategories.id, id));
}
async findCategoryByPrice(price: number): Promise<WishlistCategory | null> {
const [category] = await this.db
.select()
.from(wishlistCategories)
.where(
and(
lte(wishlistCategories.minPrice, price),
or(
gte(wishlistCategories.maxPrice, price),
isNull(wishlistCategories.maxPrice)
)
)
)
.orderBy(asc(wishlistCategories.order))
.limit(1);
return category || null;
}
private async validatePriceRange(
minPrice: number,
maxPrice: number | null | undefined,
excludeId?: string
): Promise<void> {
const categories = await this.findAll();
for (const cat of categories) {
if (excludeId && cat.id === excludeId) continue;
const catMin = cat.minPrice;
const catMax = cat.maxPrice;
// Проверить пересечение диапазонов
const hasOverlap =
(maxPrice === null || maxPrice === undefined || catMax === null || catMax === undefined)
? true // Если хотя бы один диапазон открыт сверху
: (minPrice <= catMax && maxPrice >= catMin);
if (hasOverlap && !(maxPrice !== null && maxPrice !== undefined && maxPrice < catMin)) {
throw new ConflictException(
`Price range [${minPrice}, ${maxPrice || '∞'}] overlaps with category "${cat.name}" [${catMin}, ${catMax || '∞'}]`
);
}
}
}
}

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
export const CreateWishlistCategorySchema = z.object({
name: z.string().min(1).max(100),
slug: z.string().regex(/^[a-z0-9-]+$/),
minPrice: z.number().int().min(0).default(0),
maxPrice: z.number().int().positive().optional().nullable(),
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
icon: z.string().optional(),
order: z.number().int().default(0),
});
export const UpdateWishlistCategorySchema = CreateWishlistCategorySchema.partial();
export type CreateWishlistCategoryDto = z.infer<typeof CreateWishlistCategorySchema>;
export type UpdateWishlistCategoryDto = z.infer<typeof UpdateWishlistCategorySchema>;

View File

@@ -0,0 +1,17 @@
import { z } from 'zod';
export const CreateWishlistItemSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
price: z.number().int().positive(),
currency: z.string().default('RUB'),
link: z.string().url().optional().or(z.literal('')),
// Images будут обрабатываться отдельно (файлы + URL)
imageUrls: z.array(z.string().url()).optional().default([]),
categoryId: z.string().uuid(),
});
export const UpdateWishlistItemSchema = CreateWishlistItemSchema.partial();
export type CreateWishlistItemDto = z.infer<typeof CreateWishlistItemSchema>;
export type UpdateWishlistItemDto = z.infer<typeof UpdateWishlistItemSchema>;

View File

@@ -0,0 +1,67 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseInterceptors,
UploadedFiles,
UsePipes,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { WishlistService } from './wishlist.service';
import { type CreateWishlistItemDto, type UpdateWishlistItemDto } from './dto/wishlist-item.dto';
import { ZodValidationPipe } from '../pipes/zod-validation.pipe';
import { CreateWishlistItemSchema, UpdateWishlistItemSchema } from './dto/wishlist-item.dto';
@Controller('api/wishlist')
export class WishlistController {
constructor(private readonly wishlistService: WishlistService) { }
@Get()
async findAll(@Query('categoryId') categoryId?: string) {
return this.wishlistService.findAll(categoryId);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.wishlistService.findOne(id);
}
@Post()
@UseInterceptors(FilesInterceptor('images', 5))
@UsePipes(new ZodValidationPipe(CreateWishlistItemSchema))
async create(
@Body() dto: CreateWishlistItemDto,
@UploadedFiles() files?: Express.Multer.File[]
) {
return this.wishlistService.create(dto, files);
}
@Patch(':id')
@UseInterceptors(FilesInterceptor('images', 5))
@UsePipes(new ZodValidationPipe(UpdateWishlistItemSchema))
async update(
@Param('id') id: string,
@Body() dto: UpdateWishlistItemDto,
@UploadedFiles() files?: Express.Multer.File[]
) {
return this.wishlistService.update(id, dto, files);
}
@Delete(':id')
async delete(@Param('id') id: string) {
await this.wishlistService.delete(id);
return { message: 'Wishlist item deleted successfully' };
}
@Post('upload')
@UseInterceptors(FilesInterceptor('images', 5))
async uploadImages(@UploadedFiles() files: Express.Multer.File[]) {
const paths = await this.wishlistService.uploadImages(files);
return { images: paths };
}
}

View File

@@ -0,0 +1,40 @@
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { WishlistController } from './wishlist.controller';
import { WishlistCategoriesController } from './categories.controller';
import { WishlistService } from './wishlist.service';
import { WishlistCategoriesService } from './categories.service';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [
DatabaseModule,
MulterModule.register({
storage: diskStorage({
destination: './uploads/wishlist',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
cb(null, `${uniqueSuffix}${ext}`);
},
}),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
fileFilter: (req, file, cb) => {
// Разрешить только изображения
if (file.mimetype.match(/^image\//)) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
},
}),
],
controllers: [WishlistController, WishlistCategoriesController],
providers: [WishlistService, WishlistCategoriesService],
exports: [WishlistService, WishlistCategoriesService],
})
export class WishlistModule { }

View File

@@ -0,0 +1,162 @@
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { eq } from 'drizzle-orm';
import { PgliteDatabase } from 'drizzle-orm/pglite';
import { DATABASE_CONNECTION } from '../database/database.module';
import * as schema from '../database/schema';
import { wishlistItems, WishlistItem, NewWishlistItem } from '../database/schema';
import type { CreateWishlistItemDto, UpdateWishlistItemDto } from './dto/wishlist-item.dto';
import { WishlistCategoriesService } from './categories.service';
import * as fs from 'fs/promises';
import * as path from 'path';
@Injectable()
export class WishlistService {
constructor(
@Inject(DATABASE_CONNECTION)
private readonly db: PgliteDatabase<typeof schema>,
private readonly categoriesService: WishlistCategoriesService,
) { }
async findAll(categoryId?: string): Promise<WishlistItem[]> {
const query = this.db.select().from(wishlistItems);
if (categoryId) {
query.where(eq(wishlistItems.categoryId, categoryId));
}
return query;
}
async findOne(id: string): Promise<WishlistItem> {
const [item] = await this.db
.select()
.from(wishlistItems)
.where(eq(wishlistItems.id, id))
.limit(1);
if (!item) {
throw new NotFoundException(`Wishlist item with ID ${id} not found`);
}
return item;
}
async create(
dto: CreateWishlistItemDto,
uploadedFiles?: Express.Multer.File[]
): Promise<WishlistItem> {
// Проверить существование категории
await this.categoriesService.findOne(dto.categoryId);
// Собрать массив изображений
const images: string[] = [];
// Добавить загруженные файлы
if (uploadedFiles && uploadedFiles.length > 0) {
for (const file of uploadedFiles) {
images.push(`uploaded:/uploads/wishlist/${file.filename}`);
}
}
// Добавить URL изображения
if (dto.imageUrls && dto.imageUrls.length > 0) {
for (const url of dto.imageUrls) {
images.push(`url:${url}`);
}
}
const newItem: NewWishlistItem = {
title: dto.title,
description: dto.description,
price: dto.price,
currency: dto.currency,
link: dto.link,
images,
categoryId: dto.categoryId,
createdAt: new Date(),
updatedAt: new Date(),
};
const [item] = await this.db
.insert(wishlistItems)
.values(newItem)
.returning();
return item;
}
async update(
id: string,
dto: UpdateWishlistItemDto,
uploadedFiles?: Express.Multer.File[]
): Promise<WishlistItem> {
const item = await this.findOne(id);
// Проверить существование категории если изменяется
if (dto.categoryId) {
await this.categoriesService.findOne(dto.categoryId);
}
// Обновить массив изображений
let images = [...item.images];
// Добавить новые загруженные файлы
if (uploadedFiles && uploadedFiles.length > 0) {
for (const file of uploadedFiles) {
images.push(`uploaded:/uploads/wishlist/${file.filename}`);
}
}
// Добавить новые URL
if (dto.imageUrls && dto.imageUrls.length > 0) {
for (const url of dto.imageUrls) {
if (!images.includes(`url:${url}`)) {
images.push(`url:${url}`);
}
}
}
const [updated] = await this.db
.update(wishlistItems)
.set({
...dto,
images,
updatedAt: new Date(),
})
.where(eq(wishlistItems.id, id))
.returning();
return updated;
}
async delete(id: string): Promise<void> {
const item = await this.findOne(id);
// Удалить загруженные файлы
await this.deleteUploadedImages(item.images);
await this.db
.delete(wishlistItems)
.where(eq(wishlistItems.id, id));
}
async uploadImages(files: Express.Multer.File[]): Promise<string[]> {
return files.map(file => `/uploads/wishlist/${file.filename}`);
}
private async deleteUploadedImages(images: string[]): Promise<void> {
const uploadedImages = images.filter(img => img.startsWith('uploaded:'));
for (const img of uploadedImages) {
const filePath = img.replace('uploaded:', '');
const fullPath = path.join(process.cwd(), filePath);
try {
await fs.unlink(fullPath);
} catch (error) {
// Игнорировать ошибки удаления (файл может не существовать)
console.warn(`Failed to delete file ${fullPath}:`, error);
}
}
}
}

1
backend/uploads/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# Keep this directory in git but ignore uploaded files

View File

@@ -1,5 +1,5 @@
{
"name": "@home-service/admin",
"name": "@home-service/frontend",
"version": "0.1.0",
"private": true,
"scripts": {

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,146 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
/* Terminal theme colors */
--term-bg: #0a0a0a;
--term-green: #00ff41;
--term-dim: #008f11;
--term-dark: #0d0208;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Terminal theme utilities */
--color-term-bg: var(--term-bg);
--color-term-green: var(--term-green);
--color-term-dim: var(--term-dim);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* Terminal theme classes */
.bg-term-bg {
background-color: var(--term-bg);
}
.text-term-green {
color: var(--term-green);
}
.text-term-dim {
color: var(--term-dim);
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: var(--term-bg);
border-left: 1px solid var(--term-dim);
}
::-webkit-scrollbar-thumb {
background: var(--term-dim);
}
::-webkit-scrollbar-thumb:hover {
background: var(--term-green);
}
/* Scanline effect */
.scanline {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.1)
);
background-size: 100% 4px;
pointer-events: none;
z-index: 50;
}
/* Terminal box */
.term-border {
border: 1px solid var(--term-dim);
}
.term-box {
border: 1px solid var(--term-dim);
box-shadow: 2px 2px 0px var(--term-dim);
transition: all 0.2s ease;
}
.term-box:hover {
box-shadow: 4px 4px 0px var(--term-green);
border-color: var(--term-green);
transform: translate(-2px, -2px);
}
/* Image effects */
.term-img {
filter: grayscale(100%) contrast(1.2) brightness(0.8);
transition: filter 0.3s;
}
.term-box:hover .term-img {
filter: grayscale(0%) contrast(1) brightness(1);
}
/* Cursor blinkAnimation */
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.cursor-blink {
animation: blink 1s step-end infinite;
}
/* Nav link hover */
.nav-link {
transition: all 0.2s;
}
.nav-link:hover {
background-color: var(--term-green);
color: #000;
}
.active-link {
background-color: var(--term-green);
color: #000;
font-weight: bold;
}

View File

@@ -0,0 +1,35 @@
export default function WishlistPage() {
return (
<div className="min-h-screen p-4 md:p-8 bg-term-bg text-term-green font-mono selection:bg-term-green selection:text-black">
<div className="scanline" />
<div className="max-w-7xl mx-auto">
<header className="term-border p-6 mb-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
<div>
<h1 className="text-3xl md:text-5xl font-bold mb-2">
&gt; WISHLIST_LOADED<span className="cursor-blink">_</span>
</h1>
<p className="text-term-dim text-sm md:text-base max-w-2xl">
// USER: ALEX<br />
// STATUS: WAITING_FOR_DROPS<br />
// LOCATION: EARTH_C-137<br />
<br />
Привет. Это мой список желаний. Я люблю технику, книги и старый винил.
Ниже список вещей, которые сделают меня немного счастливее.
Выбирайте любой уровень сложности (цены).
</p>
</div>
<div className="text-right hidden md:block">
<div className="text-6xl font-bold opacity-20 select-none">2025</div>
</div>
</div>
</header>
<div className="text-center text-term-dim mt-16">
<p className="mb-4">Frontend реализация в процессе...</p>
<p className="text-sm">Backend API готов на http://localhost:3000/api/wishlist</p>
</div>
</div>
</div>
);
}

View File

@@ -51,11 +51,10 @@ export function EventForm({ onEventCreated }: EventFormProps) {
setEventType('recurring');
setFormData({ ...formData, startYear: undefined, endYear: undefined, endMonth: undefined, endDay: undefined });
}}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
eventType === 'recurring'
className={`px-4 py-2 rounded-lg font-medium transition-colors ${eventType === 'recurring'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
}`}
}`}
>
Праздник (ежегодно)
</button>
@@ -65,22 +64,20 @@ export function EventForm({ onEventCreated }: EventFormProps) {
setEventType('anniversary');
setFormData({ ...formData, endYear: undefined, endMonth: undefined, endDay: undefined });
}}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
eventType === 'anniversary'
className={`px-4 py-2 rounded-lg font-medium transition-colors ${eventType === 'anniversary'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
}`}
}`}
>
Годовщина (с годом)
</button>
<button
type="button"
onClick={() => setEventType('duration')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
eventType === 'duration'
className={`px-4 py-2 rounded-lg font-medium transition-colors ${eventType === 'duration'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
}`}
}`}
>
Продолжительное
</button>
@@ -116,7 +113,7 @@ export function EventForm({ onEventCreated }: EventFormProps) {
<label className="block text-sm font-medium mb-2">Год начала</label>
<input
type="number"
required={eventType !== 'recurring'}
required
value={formData.startYear || ''}
onChange={(e) =>
setFormData({

359
pnpm-lock.yaml generated
View File

@@ -64,8 +64,11 @@ importers:
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/platform-express':
specifier: ^11.0.1
specifier: ^11.1.9
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/serve-static':
specifier: ^5.0.4
version: 5.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(express@5.1.0)
class-transformer:
specifier: ^0.5.1
version: 0.5.1
@@ -106,6 +109,9 @@ importers:
'@types/jest':
specifier: ^30.0.0
version: 30.0.0
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
'@types/node':
specifier: ^22.19.1
version: 22.19.1
@@ -136,10 +142,50 @@ importers:
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^5.7.3
version: 5.9.3
frontend:
dependencies:
next:
specifier: 16.0.5
version: 16.0.5(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react:
specifier: 19.2.0
version: 19.2.0
react-dom:
specifier: 19.2.0
version: 19.2.0(react@19.2.0)
devDependencies:
'@tailwindcss/postcss':
specifier: ^4
version: 4.1.17
'@types/node':
specifier: ^20
version: 20.19.25
'@types/react':
specifier: ^19
version: 19.2.7
'@types/react-dom':
specifier: ^19
version: 19.2.3(@types/react@19.2.7)
eslint:
specifier: ^9
version: 9.39.1(jiti@2.6.1)
eslint-config-next:
specifier: 16.0.5
version: 16.0.5(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
tailwindcss:
specifier: ^4
version: 4.1.17
typescript:
specifier: ^5
version: 5.9.3
packages:
'@alloc/quick-lru@5.2.0':
@@ -435,6 +481,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.27.1':
resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
@@ -447,6 +499,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.27.1':
resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
@@ -459,6 +517,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.27.1':
resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
@@ -471,6 +535,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.27.1':
resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
@@ -483,6 +553,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.27.1':
resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
@@ -495,6 +571,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.27.1':
resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
@@ -507,6 +589,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.27.1':
resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
@@ -519,6 +607,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.1':
resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
@@ -531,6 +625,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.27.1':
resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
@@ -543,6 +643,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.27.1':
resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
@@ -555,6 +661,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.27.1':
resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
@@ -567,6 +679,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.27.1':
resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
@@ -579,6 +697,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.27.1':
resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
@@ -591,6 +715,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.27.1':
resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
@@ -603,6 +733,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.27.1':
resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
@@ -615,6 +751,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.27.1':
resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
@@ -627,12 +769,24 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.27.1':
resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.27.1':
resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
@@ -645,12 +799,24 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.1':
resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.27.1':
resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
@@ -663,12 +829,24 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.1':
resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/openharmony-arm64@0.27.1':
resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
@@ -681,6 +859,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.27.1':
resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
@@ -693,6 +877,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.27.1':
resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
@@ -705,6 +895,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.27.1':
resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
@@ -717,6 +913,12 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.27.1':
resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1243,6 +1445,22 @@ packages:
peerDependencies:
typescript: '>=4.8.2'
'@nestjs/serve-static@5.0.4':
resolution: {integrity: sha512-3kO1M9D3vsPyWPFardxIjUYeuolS58PnhCoBTkS7t3BrdZFZCKHnBZ15js+UOzOR2Q6HmD7ssGjLd0DVYVdvOw==}
peerDependencies:
'@fastify/static': ^8.0.4
'@nestjs/common': ^11.0.2
'@nestjs/core': ^11.0.2
express: ^5.0.1
fastify: ^5.2.1
peerDependenciesMeta:
'@fastify/static':
optional: true
express:
optional: true
fastify:
optional: true
'@nestjs/testing@11.1.9':
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
peerDependencies:
@@ -1531,6 +1749,9 @@ packages:
'@types/methods@1.1.4':
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
'@types/multer@2.0.0':
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
'@types/node@20.19.25':
resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==}
@@ -2482,6 +2703,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
esbuild@0.27.1:
resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -4265,6 +4491,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
hasBin: true
turbo-darwin-64@2.6.3:
resolution: {integrity: sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg==}
cpu: [x64]
@@ -4851,147 +5082,225 @@ snapshots:
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/aix-ppc64@0.27.1':
optional: true
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm64@0.27.1':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-arm@0.27.1':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/android-x64@0.27.1':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.27.1':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.27.1':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.27.1':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.27.1':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.27.1':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-arm@0.27.1':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-ia32@0.27.1':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-loong64@0.27.1':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.27.1':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.27.1':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.27.1':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-s390x@0.27.1':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/linux-x64@0.27.1':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.27.1':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.27.1':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.27.1':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.27.1':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.27.1':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.27.1':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.27.1':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-ia32@0.27.1':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@esbuild/win32-x64@0.27.1':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
dependencies:
eslint: 9.39.1(jiti@2.6.1)
@@ -5614,6 +5923,14 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@nestjs/serve-static@5.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(express@5.1.0)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
path-to-regexp: 8.3.0
optionalDependencies:
express: 5.1.0
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -5869,6 +6186,10 @@ snapshots:
'@types/methods@1.1.4': {}
'@types/multer@2.0.0':
dependencies:
'@types/express': 5.0.6
'@types/node@20.19.25':
dependencies:
undici-types: 6.21.0
@@ -6879,6 +7200,35 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
esbuild@0.27.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.1
'@esbuild/android-arm': 0.27.1
'@esbuild/android-arm64': 0.27.1
'@esbuild/android-x64': 0.27.1
'@esbuild/darwin-arm64': 0.27.1
'@esbuild/darwin-x64': 0.27.1
'@esbuild/freebsd-arm64': 0.27.1
'@esbuild/freebsd-x64': 0.27.1
'@esbuild/linux-arm': 0.27.1
'@esbuild/linux-arm64': 0.27.1
'@esbuild/linux-ia32': 0.27.1
'@esbuild/linux-loong64': 0.27.1
'@esbuild/linux-mips64el': 0.27.1
'@esbuild/linux-ppc64': 0.27.1
'@esbuild/linux-riscv64': 0.27.1
'@esbuild/linux-s390x': 0.27.1
'@esbuild/linux-x64': 0.27.1
'@esbuild/netbsd-arm64': 0.27.1
'@esbuild/netbsd-x64': 0.27.1
'@esbuild/openbsd-arm64': 0.27.1
'@esbuild/openbsd-x64': 0.27.1
'@esbuild/openharmony-arm64': 0.27.1
'@esbuild/sunos-x64': 0.27.1
'@esbuild/win32-arm64': 0.27.1
'@esbuild/win32-ia32': 0.27.1
'@esbuild/win32-x64': 0.27.1
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -9029,6 +9379,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.1
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
turbo-darwin-64@2.6.3:
optional: true

View File

@@ -1,3 +1,3 @@
packages:
- 'admin'
- 'frontend'
- 'backend'