Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 13939c3

Browse files
committed
feat: Add OrganizationUpdate module with CRUD operations and validation for organization updates
1 parent 4727c69 commit 13939c3

7 files changed

Lines changed: 313 additions & 0 deletions

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { QuestModule } from './quest/quest.module';
1414
import { UploadModule } from './upload/upload.module';
1515
import { CategoryModule } from './category/category.module';
1616
import { QuestUpdateModule } from './quest-update/quest-update.module';
17+
import { OrganizationUpdateModule } from './organization-update/organization-update.module';
1718
import { AppController } from './app.controller';
1819

1920
@Module({
@@ -37,6 +38,7 @@ import { AppController } from './app.controller';
3738
UploadModule,
3839
CategoryModule,
3940
QuestUpdateModule,
41+
OrganizationUpdateModule,
4042
],
4143
controllers: [AppController],
4244
})

src/database/schema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ export const questUpdates = pgTable('quest_updates', {
192192
updatedAt: timestamp('updated_at').defaultNow(),
193193
});
194194

195+
// Обновления организаций
196+
export const organizationUpdates = pgTable('organization_updates', {
197+
id: serial('id').primaryKey(),
198+
organizationId: integer('organization_id')
199+
.references(() => organizations.id)
200+
.notNull(),
201+
title: varchar('title', { length: 255 }).notNull(),
202+
text: text('text').notNull(),
203+
photos: jsonb('photos').$type<string[]>(),
204+
recordStatus: varchar('record_status', { length: 20 }).default('CREATED').notNull(),
205+
createdAt: timestamp('created_at').defaultNow(),
206+
updatedAt: timestamp('updated_at').defaultNow(),
207+
});
208+
195209
// Связующая таблица: выполнение квестов пользователями
196210
export const userQuests = pgTable('user_quests', {
197211
id: serial('id').primaryKey(),
@@ -257,6 +271,7 @@ export const organizationsRelations = relations(organizations, ({ one, many }) =
257271
}),
258272
owners: many(organizationOwners),
259273
helpTypes: many(organizationHelpTypes),
274+
updates: many(organizationUpdates),
260275
}));
261276

262277
export const organizationOwnersRelations = relations(organizationOwners, ({ one }) => ({
@@ -340,6 +355,13 @@ export const questUpdatesRelations = relations(questUpdates, ({ one }) => ({
340355
}),
341356
}));
342357

358+
export const organizationUpdatesRelations = relations(organizationUpdates, ({ one }) => ({
359+
organization: one(organizations, {
360+
fields: [organizationUpdates.organizationId],
361+
references: [organizations.id],
362+
}),
363+
}));
364+
343365
export const userQuestsRelations = relations(userQuests, ({ one }) => ({
344366
user: one(users, {
345367
fields: [userQuests.userId],
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { z } from 'zod/v4';
3+
4+
export const createOrganizationUpdateSchema = z.object({
5+
organizationId: z.number().int().positive('ID организации должен быть положительным числом'),
6+
title: z.string().min(1, 'Название обновления обязательно').max(255, 'Название не должно превышать 255 символов'),
7+
text: z.string().min(1, 'Текст обновления обязателен'),
8+
photos: z.array(z.string().url('Элемент должен быть валидным URL')).max(5, 'Максимум 5 фотографий').optional(),
9+
});
10+
11+
export type CreateOrganizationUpdateDto = z.infer<typeof createOrganizationUpdateSchema>;
12+
13+
export class CreateOrganizationUpdateDtoClass {
14+
@ApiProperty({ description: 'ID организации', example: 1 })
15+
organizationId: number;
16+
17+
@ApiProperty({ description: 'Название обновления', example: 'Новые проекты организации' })
18+
title: string;
19+
20+
@ApiProperty({ description: 'Текст обновления', example: 'Запущен новый проект помощи бездомным' })
21+
text: string;
22+
23+
@ApiProperty({
24+
description: 'Фотографии обновления (максимум 5)',
25+
example: ['https://example.com/photo1.jpg', 'https://example.com/photo2.jpg'],
26+
type: [String],
27+
maxItems: 5,
28+
required: false
29+
})
30+
photos?: string[];
31+
}
32+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { z } from 'zod/v4';
3+
4+
export const updateOrganizationUpdateSchema = z.object({
5+
organizationId: z.number().int().positive('ID организации должен быть положительным числом').optional(),
6+
title: z.string().max(255, 'Название не должно превышать 255 символов').optional(),
7+
text: z.string().optional(),
8+
photos: z.array(z.string().url('Элемент должен быть валидным URL')).max(5, 'Максимум 5 фотографий').optional(),
9+
});
10+
11+
export type UpdateOrganizationUpdateDto = z.infer<typeof updateOrganizationUpdateSchema>;
12+
13+
export class UpdateOrganizationUpdateDtoClass {
14+
@ApiProperty({ description: 'ID организации', example: 1, required: false })
15+
organizationId?: number;
16+
17+
@ApiProperty({ description: 'Название обновления', example: 'Новые проекты организации', required: false })
18+
title?: string;
19+
20+
@ApiProperty({ description: 'Текст обновления', example: 'Запущен новый проект помощи бездомным', required: false })
21+
text?: string;
22+
23+
@ApiProperty({
24+
description: 'Фотографии обновления (максимум 5)',
25+
example: ['https://example.com/photo1.jpg'],
26+
type: [String],
27+
maxItems: 5,
28+
required: false
29+
})
30+
photos?: string[];
31+
}
32+
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Body,
6+
Patch,
7+
Param,
8+
Delete,
9+
Query,
10+
ParseIntPipe,
11+
UseGuards,
12+
} from '@nestjs/common';
13+
import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
14+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
15+
import { OrganizationUpdateService } from './organization-update.service';
16+
import { CreateOrganizationUpdateDto, createOrganizationUpdateSchema, CreateOrganizationUpdateDtoClass } from './dto/create-organization-update.dto';
17+
import { UpdateOrganizationUpdateDto, updateOrganizationUpdateSchema, UpdateOrganizationUpdateDtoClass } from './dto/update-organization-update.dto';
18+
import { ZodValidation } from '../common/decorators/zod-validation.decorator';
19+
20+
@ApiTags('Обновления организаций')
21+
@Controller('organization-updates')
22+
export class OrganizationUpdateController {
23+
constructor(private readonly organizationUpdateService: OrganizationUpdateService) {}
24+
25+
@Post()
26+
@ZodValidation(createOrganizationUpdateSchema)
27+
@ApiOperation({ summary: 'Создать обновление организации' })
28+
@ApiBody({ type: CreateOrganizationUpdateDtoClass })
29+
@ApiResponse({ status: 201, description: 'Обновление организации успешно создано', type: CreateOrganizationUpdateDtoClass })
30+
@ApiResponse({ status: 400, description: 'Ошибка валидации' })
31+
@ApiResponse({ status: 404, description: 'Организация не найдена' })
32+
@ApiBearerAuth()
33+
@UseGuards(JwtAuthGuard)
34+
create(@Body() createOrganizationUpdateDto: CreateOrganizationUpdateDto) {
35+
return this.organizationUpdateService.create(createOrganizationUpdateDto);
36+
}
37+
38+
@Get()
39+
@UseGuards(JwtAuthGuard)
40+
@ApiBearerAuth()
41+
@ApiOperation({ summary: 'Получить все обновления организаций' })
42+
@ApiQuery({
43+
name: 'organizationId',
44+
required: false,
45+
type: Number,
46+
description: 'ID организации для фильтрации'
47+
})
48+
@ApiResponse({ status: 200, description: 'Список обновлений организаций' })
49+
@ApiResponse({ status: 401, description: 'Не авторизован' })
50+
findAll(@Query('organizationId', new ParseIntPipe({ optional: true })) organizationId?: number) {
51+
return this.organizationUpdateService.findAll(organizationId);
52+
}
53+
54+
@Get(':id')
55+
@UseGuards(JwtAuthGuard)
56+
@ApiBearerAuth()
57+
@ApiOperation({ summary: 'Получить обновление организации по ID' })
58+
@ApiResponse({ status: 200, description: 'Обновление организации найдено' })
59+
@ApiResponse({ status: 404, description: 'Обновление организации не найдено' })
60+
@ApiResponse({ status: 401, description: 'Не авторизован' })
61+
findOne(@Param('id', ParseIntPipe) id: number) {
62+
return this.organizationUpdateService.findOne(id);
63+
}
64+
65+
@Patch(':id')
66+
@ApiBearerAuth()
67+
@UseGuards(JwtAuthGuard)
68+
@ZodValidation(updateOrganizationUpdateSchema)
69+
@ApiOperation({ summary: 'Обновить обновление организации' })
70+
@ApiBody({ type: UpdateOrganizationUpdateDtoClass })
71+
@ApiResponse({ status: 200, description: 'Обновление организации обновлено', type: UpdateOrganizationUpdateDtoClass })
72+
@ApiResponse({ status: 400, description: 'Ошибка валидации' })
73+
@ApiResponse({ status: 404, description: 'Обновление организации или организация не найдены' })
74+
update(
75+
@Param('id', ParseIntPipe) id: number,
76+
@Body() updateOrganizationUpdateDto: UpdateOrganizationUpdateDto,
77+
) {
78+
return this.organizationUpdateService.update(id, updateOrganizationUpdateDto);
79+
}
80+
81+
@Delete(':id')
82+
@UseGuards(JwtAuthGuard)
83+
@ApiBearerAuth()
84+
@ApiOperation({ summary: 'Удалить обновление организации' })
85+
@ApiResponse({ status: 200, description: 'Обновление организации удалено' })
86+
@ApiResponse({ status: 404, description: 'Обновление организации не найдено' })
87+
@ApiResponse({ status: 401, description: 'Не авторизован' })
88+
remove(@Param('id', ParseIntPipe) id: number) {
89+
return this.organizationUpdateService.remove(id);
90+
}
91+
}
92+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { OrganizationUpdateService } from './organization-update.service';
3+
import { OrganizationUpdateController } from './organization-update.controller';
4+
import { DatabaseModule } from '../database/database.module';
5+
6+
@Module({
7+
imports: [DatabaseModule],
8+
controllers: [OrganizationUpdateController],
9+
providers: [OrganizationUpdateService],
10+
exports: [OrganizationUpdateService],
11+
})
12+
export class OrganizationUpdateModule {}
13+
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common';
2+
import { DATABASE_CONNECTION } from '../database/database.module';
3+
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
4+
import { organizationUpdates, organizations } from '../database/schema';
5+
import { eq, ne, and } from 'drizzle-orm';
6+
import { CreateOrganizationUpdateDto } from './dto/create-organization-update.dto';
7+
import { UpdateOrganizationUpdateDto } from './dto/update-organization-update.dto';
8+
9+
@Injectable()
10+
export class OrganizationUpdateService {
11+
constructor(
12+
@Inject(DATABASE_CONNECTION)
13+
private db: NodePgDatabase,
14+
) {}
15+
16+
async create(createOrganizationUpdateDto: CreateOrganizationUpdateDto) {
17+
// Проверяем существование организации (исключая удаленные)
18+
const [organization] = await this.db
19+
.select()
20+
.from(organizations)
21+
.where(and(
22+
eq(organizations.id, createOrganizationUpdateDto.organizationId),
23+
ne(organizations.recordStatus, 'DELETED')
24+
));
25+
if (!organization) {
26+
throw new NotFoundException(`Организация с ID ${createOrganizationUpdateDto.organizationId} не найдена`);
27+
}
28+
29+
// Валидация фотографий (максимум 5 элементов)
30+
if (createOrganizationUpdateDto.photos && createOrganizationUpdateDto.photos.length > 5) {
31+
throw new BadRequestException('Максимум 5 фотографий');
32+
}
33+
34+
const [organizationUpdate] = await this.db
35+
.insert(organizationUpdates)
36+
.values({
37+
organizationId: createOrganizationUpdateDto.organizationId,
38+
title: createOrganizationUpdateDto.title,
39+
text: createOrganizationUpdateDto.text,
40+
photos: createOrganizationUpdateDto.photos || [],
41+
})
42+
.returning();
43+
return organizationUpdate;
44+
}
45+
46+
async findAll(organizationId?: number) {
47+
const conditions = [ne(organizationUpdates.recordStatus, 'DELETED')];
48+
if (organizationId) {
49+
conditions.push(eq(organizationUpdates.organizationId, organizationId));
50+
}
51+
return this.db
52+
.select()
53+
.from(organizationUpdates)
54+
.where(and(...conditions));
55+
}
56+
57+
async findOne(id: number) {
58+
const [organizationUpdate] = await this.db
59+
.select()
60+
.from(organizationUpdates)
61+
.where(and(
62+
eq(organizationUpdates.id, id),
63+
ne(organizationUpdates.recordStatus, 'DELETED')
64+
));
65+
if (!organizationUpdate) {
66+
throw new NotFoundException(`Обновление организации с ID ${id} не найдено`);
67+
}
68+
return organizationUpdate;
69+
}
70+
71+
async update(id: number, updateOrganizationUpdateDto: UpdateOrganizationUpdateDto) {
72+
// Если обновляется organizationId, проверяем существование организации (исключая удаленные)
73+
if (updateOrganizationUpdateDto.organizationId !== undefined) {
74+
const [organization] = await this.db
75+
.select()
76+
.from(organizations)
77+
.where(and(
78+
eq(organizations.id, updateOrganizationUpdateDto.organizationId),
79+
ne(organizations.recordStatus, 'DELETED')
80+
));
81+
if (!organization) {
82+
throw new NotFoundException(`Организация с ID ${updateOrganizationUpdateDto.organizationId} не найдена`);
83+
}
84+
}
85+
86+
// Валидация фотографий (максимум 5 элементов)
87+
if (updateOrganizationUpdateDto.photos && updateOrganizationUpdateDto.photos.length > 5) {
88+
throw new BadRequestException('Максимум 5 фотографий');
89+
}
90+
91+
const [organizationUpdate] = await this.db
92+
.update(organizationUpdates)
93+
.set({ ...updateOrganizationUpdateDto, updatedAt: new Date() })
94+
.where(and(
95+
eq(organizationUpdates.id, id),
96+
ne(organizationUpdates.recordStatus, 'DELETED')
97+
))
98+
.returning();
99+
if (!organizationUpdate) {
100+
throw new NotFoundException(`Обновление организации с ID ${id} не найдено`);
101+
}
102+
return organizationUpdate;
103+
}
104+
105+
async remove(id: number) {
106+
const [organizationUpdate] = await this.db
107+
.update(organizationUpdates)
108+
.set({ recordStatus: 'DELETED', updatedAt: new Date() })
109+
.where(and(
110+
eq(organizationUpdates.id, id),
111+
ne(organizationUpdates.recordStatus, 'DELETED')
112+
))
113+
.returning();
114+
if (!organizationUpdate) {
115+
throw new NotFoundException(`Обновление организации с ID ${id} не найдено`);
116+
}
117+
return organizationUpdate;
118+
}
119+
}
120+

0 commit comments

Comments
 (0)