Skip to content

Commit b3f515f

Browse files
author
Benny Burkert
committed
feat: Create prompt breakthrough (Circular dependency)
1 parent 10abed8 commit b3f515f

12 files changed

Lines changed: 457 additions & 13 deletions

File tree

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ExtensionsController } from './controllers/extensions/extensions.contro
1414
import { FilesController } from './controllers/files/files.controller';
1515
import { UserFilesController } from './controllers/files/user-files.controller';
1616
import { HealthController } from './controllers/health/health.controller';
17+
import { PromptController } from './controllers/prompts/prompts.controller';
1718
import { SettingsController } from './controllers/settings/settings.controller';
1819
import { UsagesController } from './controllers/usages/usages.controller';
1920
import { UserGroupsController } from './controllers/users/user-groups.controller';
@@ -24,6 +25,7 @@ import { UserEntity } from './domain/database';
2425
import { initSchemaIfNotExistsAndMoveMigrations, schema } from './domain/database/typeorm.helper';
2526
import { ExtensionModule } from './domain/extensions';
2627
import { FilesModule } from './domain/files';
28+
import { PromptModule } from './domain/prompt/module';
2729
import { SettingsModule } from './domain/settings';
2830
import { UsersModule } from './domain/users/module';
2931
import { ExtensionLibraryModule } from './extensions';
@@ -47,6 +49,7 @@ import { PrometheusModule } from './metrics/prometheus.module';
4749
},
4850
}),
4951
PrometheusModule.forRoot(),
52+
PromptModule,
5053
SettingsModule,
5154
UsersModule,
5255
TerminusModule,
@@ -82,6 +85,7 @@ import { PrometheusModule } from './metrics/prometheus.module';
8285
ConfigurationsController,
8386
ExtensionsController,
8487
FilesController,
88+
PromptController,
8589
SettingsController,
8690
UsagesController,
8791
UserFilesController,

backend/src/controllers/prompts/dtos/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class CreatePromptDto {
7373
})
7474
@IsOptional()
7575
@IsArray()
76-
categories?: [number];
76+
categories?: string[];
7777

7878
@ApiProperty({
7979
description: 'The visibility of the prompt (e.g., public, private).',

backend/src/controllers/prompts/prompts.controller.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
2+
import { CommandBus } from '@nestjs/cqrs';
23
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
4+
import { CreatePrompt, CreatePromptResponse } from 'src/domain/prompt/use-cases/create-prompt';
35
import { CreatePromptCategoryDto, CreatePromptDto, PromptCategoryDto, PromptDto } from './dtos/index';
46

57
@Controller('prompt')
68
export class PromptController {
9+
constructor(private readonly commandBus: CommandBus) {}
10+
711
@Post('categories')
812
@ApiOperation({ operationId: 'postPromptCategory', summary: 'Create a new prompt category' })
913
@ApiResponse({ status: 201, description: 'Category created successfully' })
@@ -22,7 +26,28 @@ export class PromptController {
2226
@ApiOperation({ operationId: 'postPrompt', summary: 'Create a new prompt' })
2327
@ApiResponse({ status: 201, description: 'Prompt created successfully' })
2428
async createPrompt(@Body() createPromptDto: CreatePromptDto): Promise<PromptDto> {
25-
return Promise.resolve(undefined as unknown as PromptDto);
29+
const response: CreatePromptResponse = await this.commandBus.execute(
30+
new CreatePrompt({
31+
title: createPromptDto.title,
32+
description: createPromptDto.description,
33+
content: createPromptDto.content,
34+
visibility: createPromptDto.visibility,
35+
categoryLabels: createPromptDto.categories,
36+
}),
37+
);
38+
39+
// Map entity to DTO
40+
const promptDto: PromptDto = {
41+
id: response.prompt.id.toString(),
42+
title: response.prompt.title,
43+
description: response.prompt.description,
44+
content: response.prompt.content,
45+
visibility: response.prompt.visibility,
46+
categories: response.prompt.categories?.map(cat => cat.label) || [],
47+
rating: response.prompt.rating,
48+
};
49+
50+
return promptDto;
2651
}
2752

2853
@Get()
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Server } from 'net';
2+
import { HttpStatus, INestApplication } from '@nestjs/common';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
import * as request from 'supertest';
5+
import { DataSource, Repository } from 'typeorm';
6+
import { AppModule } from '../../app.module';
7+
import { PromptCategoryEntity } from '../../domain/database';
8+
import { VisibilityType } from '../../domain/prompt';
9+
import { initAppWithDataBaseAndValidUser } from '../../utils/testUtils';
10+
import { CreatePromptDto, PromptDto } from './dtos';
11+
12+
describe('Prompts', () => {
13+
let app: INestApplication<Server>;
14+
let dataSource: DataSource;
15+
16+
beforeAll(async () => {
17+
const module: TestingModule = await Test.createTestingModule({
18+
imports: [AppModule],
19+
}).compile();
20+
21+
const initialized = await initAppWithDataBaseAndValidUser(dataSource, module, app);
22+
dataSource = initialized.dataSource;
23+
app = initialized.app;
24+
await seedTestData(dataSource);
25+
});
26+
27+
afterAll(async () => {
28+
await dataSource.destroy();
29+
await app.close();
30+
});
31+
32+
it('should create a prompt without categories', async () => {
33+
const newPrompt: CreatePromptDto = {
34+
title: 'Test Prompt',
35+
description: 'A test prompt for e2e testing',
36+
content: 'This is the prompt content for testing purposes.',
37+
visibility: VisibilityType.PUBLIC,
38+
};
39+
40+
const response = await request(app.getHttpServer()).post('/prompt').send(newPrompt).expect(HttpStatus.CREATED);
41+
42+
const typedBody = response.body as PromptDto;
43+
expect(typedBody.id).toBeDefined();
44+
expect(typedBody.title).toBe(newPrompt.title);
45+
expect(typedBody.description).toBe(newPrompt.description);
46+
expect(typedBody.content).toBe(newPrompt.content);
47+
expect(typedBody.visibility).toBe(newPrompt.visibility);
48+
expect(typedBody.categories).toEqual([]);
49+
});
50+
51+
it('should create a prompt with categories', async () => {
52+
const newPrompt: CreatePromptDto = {
53+
title: 'Categorized Prompt',
54+
content: 'This prompt has categories.',
55+
visibility: VisibilityType.PRIVATE,
56+
categories: ['technical', 'creative'],
57+
};
58+
59+
const response = await request(app.getHttpServer()).post('/prompt').send(newPrompt).expect(HttpStatus.CREATED);
60+
61+
const typedBody = response.body as PromptDto;
62+
expect(typedBody.id).toBeDefined();
63+
expect(typedBody.title).toBe(newPrompt.title);
64+
expect(typedBody.content).toBe(newPrompt.content);
65+
expect(typedBody.visibility).toBe(newPrompt.visibility);
66+
expect(typedBody.categories).toHaveLength(2);
67+
expect(typedBody.categories).toEqual(
68+
expect.arrayContaining([expect.objectContaining({ label: 'technical' }), expect.objectContaining({ label: 'creative' })]),
69+
);
70+
});
71+
72+
it('should create a prompt with only required fields', async () => {
73+
const newPrompt: CreatePromptDto = {
74+
title: 'Minimal Prompt',
75+
content: 'This is a minimal prompt with only required fields.',
76+
visibility: VisibilityType.PRIVATE,
77+
};
78+
79+
const response = await request(app.getHttpServer()).post('/prompt').send(newPrompt).expect(HttpStatus.CREATED);
80+
81+
const typedBody = response.body as PromptDto;
82+
expect(typedBody.id).toBeDefined();
83+
expect(typedBody.title).toBe(newPrompt.title);
84+
expect(typedBody.content).toBe(newPrompt.content);
85+
expect(typedBody.visibility).toBe(newPrompt.visibility);
86+
expect(typedBody.description).toBeUndefined();
87+
expect(typedBody.rating).toBeUndefined();
88+
expect(typedBody.categories).toEqual([]);
89+
});
90+
91+
it('should create a prompt with non-existent categories', async () => {
92+
const newPrompt: CreatePromptDto = {
93+
title: 'Prompt with Non-existent Categories',
94+
content: 'This prompt references categories that do not exist.',
95+
visibility: VisibilityType.PUBLIC,
96+
categories: ['nonexistent1', 'technical', 'nonexistent2'],
97+
};
98+
99+
const response = await request(app.getHttpServer()).post('/prompt').send(newPrompt).expect(HttpStatus.CREATED);
100+
101+
const typedBody = response.body as PromptDto;
102+
expect(typedBody.categories).toHaveLength(1); // Only 'technical' exists
103+
expect(typedBody.categories).toEqual([expect.objectContaining({ label: 'technical' })]);
104+
});
105+
106+
it('should fail to create a prompt without required fields', async () => {
107+
const invalidPrompt = {
108+
description: 'Missing title and content',
109+
visibility: VisibilityType.PUBLIC,
110+
};
111+
112+
await request(app.getHttpServer()).post('/prompt').send(invalidPrompt).expect(HttpStatus.BAD_REQUEST);
113+
});
114+
115+
it('should fail to create a prompt with invalid visibility', async () => {
116+
const invalidPrompt = {
117+
title: 'Invalid Prompt',
118+
content: 'This prompt has invalid visibility.',
119+
visibility: 'invalid_visibility',
120+
};
121+
122+
await request(app.getHttpServer()).post('/prompt').send(invalidPrompt).expect(HttpStatus.BAD_REQUEST);
123+
});
124+
});
125+
126+
async function seedTestData(dataSource: DataSource) {
127+
const promptCategoryRepository = dataSource.getRepository(PromptCategoryEntity);
128+
129+
// Create test categories
130+
await createPromptCategory('technical', 'Technical prompts', promptCategoryRepository);
131+
await createPromptCategory('creative', 'Creative writing prompts', promptCategoryRepository);
132+
await createPromptCategory('business', 'Business and professional prompts', promptCategoryRepository);
133+
}
134+
135+
async function createPromptCategory(
136+
label: string,
137+
description: string,
138+
promptCategoryRepository: Repository<PromptCategoryEntity>,
139+
): Promise<PromptCategoryEntity> {
140+
const categoryEntity = new PromptCategoryEntity();
141+
categoryEntity.label = label;
142+
categoryEntity.description = description;
143+
categoryEntity.creationDate = new Date();
144+
categoryEntity.visibility = VisibilityType.PUBLIC;
145+
return promptCategoryRepository.save(categoryEntity);
146+
}

backend/src/domain/database/entities/prompt-category.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Column, Entity, PrimaryColumn } from 'typeorm';
2-
import { VisibilityType } from '../../prompt';
1+
import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
2+
import { VisibilityType } from '../../prompt/interfaces';
33
import { schema } from '../typeorm.helper';
44

55
@Entity({ name: 'prompt-categories', schema })
@@ -15,4 +15,10 @@ export class PromptCategoryEntity {
1515

1616
@Column()
1717
visibility!: VisibilityType;
18+
19+
@CreateDateColumn({ type: 'timestamptz' })
20+
createdAt!: Date;
21+
22+
@UpdateDateColumn({ type: 'timestamptz' })
23+
updatedAt!: Date;
1824
}

backend/src/domain/database/entities/prompt.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
2-
import { VisibilityType } from '../../prompt';
1+
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
2+
import { VisibilityType } from '../../prompt/interfaces';
33
import { schema } from '../typeorm.helper';
44
import { PromptCategoryEntity } from './prompt-category';
55

66
@Entity({ name: 'prompts', schema })
77
export class PromptEntity {
88
@PrimaryGeneratedColumn()
9-
id!: string;
9+
id!: number;
1010

11-
@Column({ nullable: true })
11+
@Column({ nullable: false })
1212
title!: string;
1313

1414
@Column({ nullable: true })
@@ -25,5 +25,11 @@ export class PromptEntity {
2525
visibility!: VisibilityType;
2626

2727
@Column({ nullable: true })
28-
raiting!: number;
28+
rating!: number;
29+
30+
@CreateDateColumn({ type: 'timestamptz' })
31+
createdAt!: Date;
32+
33+
@UpdateDateColumn({ type: 'timestamptz' })
34+
updatedAt!: Date;
2935
}

backend/src/domain/prompt/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './interfaces';
2+
export * from './module';

backend/src/domain/prompt/interfaces.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,52 @@ export enum VisibilityType {
22
PRIVATE = 'private',
33
PUBLIC = 'public',
44
}
5+
6+
export interface Prompt {
7+
// The prompt ID.
8+
id: number;
9+
10+
// The prompt title.
11+
title: string;
12+
13+
// Optional description of the prompt.
14+
description?: string;
15+
16+
// The actual prompt content.
17+
content: string;
18+
19+
// Visibility setting for the prompt.
20+
visibility: VisibilityType;
21+
22+
// Optional rating for the prompt.
23+
rating?: number;
24+
25+
// Associated categories.
26+
categories?: PromptCategory[];
27+
28+
// Creation timestamp.
29+
createdAt: Date;
30+
31+
// Last update timestamp.
32+
updatedAt: Date;
33+
}
34+
35+
export interface PromptCategory {
36+
// The category label (primary key).
37+
label: string;
38+
39+
// Optional description of the category.
40+
description?: string;
41+
42+
// Manual creation date field.
43+
creationDate: Date;
44+
45+
// Visibility setting for the category.
46+
visibility: VisibilityType;
47+
48+
// Creation timestamp.
49+
createdAt: Date;
50+
51+
// Last update timestamp.
52+
updatedAt: Date;
53+
}
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 { CqrsModule } from '@nestjs/cqrs';
3+
import { TypeOrmModule } from '@nestjs/typeorm';
4+
import { PromptCategoryEntity } from '../database/entities/prompt-category';
5+
import { PromptEntity } from '../database/entities/prompt';
6+
import { CreatePromptHandler } from './use-cases/create-prompt';
7+
8+
@Module({
9+
imports: [CqrsModule, TypeOrmModule.forFeature([PromptEntity, PromptCategoryEntity])],
10+
providers: [CreatePromptHandler],
11+
exports: [CreatePromptHandler],
12+
})
13+
export class PromptModule {}

0 commit comments

Comments
 (0)