fragment-ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -0
- package/base.ts +1810 -0
- package/base2.ts +968 -0
- package/bin/frg.ts +5 -0
- package/config/fragment.lock.yaml +0 -0
- package/config/fragment.yaml +0 -0
- package/dist/app.d.ts +15 -0
- package/dist/app.js +90 -0
- package/dist/auth/auth.controller.d.ts +10 -0
- package/dist/auth/auth.controller.js +87 -0
- package/dist/auth/auth.middleware.d.ts +2 -0
- package/dist/auth/auth.middleware.js +24 -0
- package/dist/auth/auth.service.d.ts +20 -0
- package/dist/auth/auth.service.js +143 -0
- package/dist/auth/dto/login.dto.d.ts +9 -0
- package/dist/auth/dto/login.dto.js +2 -0
- package/dist/cli/cli.d.ts +12 -0
- package/dist/cli/cli.js +186 -0
- package/dist/cli/commands/build.command.d.ts +3 -0
- package/dist/cli/commands/build.command.js +23 -0
- package/dist/cli/commands/config.command.d.ts +6 -0
- package/dist/cli/commands/config.command.js +284 -0
- package/dist/cli/commands/generate.command.d.ts +8 -0
- package/dist/cli/commands/generate.command.js +180 -0
- package/dist/cli/commands/init.command.d.ts +7 -0
- package/dist/cli/commands/init.command.js +380 -0
- package/dist/cli/commands/migrate.command.d.ts +7 -0
- package/dist/cli/commands/migrate.command.js +116 -0
- package/dist/cli/commands/serve.command.d.ts +6 -0
- package/dist/cli/commands/serve.command.js +31 -0
- package/dist/cli/templates/controller.template.d.ts +1 -0
- package/dist/cli/templates/controller.template.js +52 -0
- package/dist/cli/templates/entity.template.d.ts +1 -0
- package/dist/cli/templates/entity.template.js +23 -0
- package/dist/cli/templates/repository.template.d.ts +1 -0
- package/dist/cli/templates/repository.template.js +43 -0
- package/dist/cli/templates/service.template.d.ts +1 -0
- package/dist/cli/templates/service.template.js +43 -0
- package/dist/cli/utils/file-generator.d.ts +9 -0
- package/dist/cli/utils/file-generator.js +67 -0
- package/dist/cli/utils/logger.d.ts +14 -0
- package/dist/cli/utils/logger.js +49 -0
- package/dist/controllers/health.controller.d.ts +13 -0
- package/dist/controllers/health.controller.js +50 -0
- package/dist/core/config/config-loader.d.ts +31 -0
- package/dist/core/config/config-loader.js +98 -0
- package/dist/core/container/di-container.d.ts +9 -0
- package/dist/core/container/di-container.js +37 -0
- package/dist/core/decorators/auth-guard.decorator.d.ts +3 -0
- package/dist/core/decorators/auth-guard.decorator.js +18 -0
- package/dist/core/decorators/autowire.decorator.d.ts +3 -0
- package/dist/core/decorators/autowire.decorator.js +17 -0
- package/dist/core/decorators/controller.decorator.d.ts +4 -0
- package/dist/core/decorators/controller.decorator.js +16 -0
- package/dist/core/decorators/injectable.decorator.d.ts +3 -0
- package/dist/core/decorators/injectable.decorator.js +14 -0
- package/dist/core/decorators/middleware.decorator.d.ts +3 -0
- package/dist/core/decorators/middleware.decorator.js +20 -0
- package/dist/core/decorators/repository.decorator.d.ts +1 -0
- package/dist/core/decorators/repository.decorator.js +7 -0
- package/dist/core/decorators/route.decorator.d.ts +14 -0
- package/dist/core/decorators/route.decorator.js +32 -0
- package/dist/core/decorators/service.decorator.d.ts +1 -0
- package/dist/core/decorators/service.decorator.js +7 -0
- package/dist/core/openai/openai-client.d.ts +12 -0
- package/dist/core/openai/openai-client.js +93 -0
- package/dist/database/data-source.d.ts +4 -0
- package/dist/database/data-source.js +26 -0
- package/dist/entities/session.entity.d.ts +9 -0
- package/dist/entities/session.entity.js +45 -0
- package/dist/entities/user.entity.d.ts +10 -0
- package/dist/entities/user.entity.js +48 -0
- package/dist/middlewares/logging.middleware.d.ts +2 -0
- package/dist/middlewares/logging.middleware.js +28 -0
- package/dist/repositories/session.repository.d.ts +9 -0
- package/dist/repositories/session.repository.js +50 -0
- package/dist/repositories/user.repository.d.ts +10 -0
- package/dist/repositories/user.repository.js +43 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +30 -0
- package/dist/services/health.service.d.ts +13 -0
- package/dist/services/health.service.js +44 -0
- package/package.json +46 -0
- package/readme.md +120 -0
- package/src/app.ts +121 -0
- package/src/auth/auth.controller.ts +52 -0
- package/src/auth/auth.middleware.ts +27 -0
- package/src/auth/auth.service.ts +110 -0
- package/src/auth/dto/login.dto.ts +11 -0
- package/src/cli/cli.ts +212 -0
- package/src/cli/commands/build.command.ts +24 -0
- package/src/cli/commands/config.command.ts +280 -0
- package/src/cli/commands/generate.command.ts +170 -0
- package/src/cli/commands/init.command.ts +395 -0
- package/src/cli/commands/migrate.command.ts +118 -0
- package/src/cli/commands/serve.command.ts +37 -0
- package/src/cli/templates/controller.template.ts +51 -0
- package/src/cli/templates/entity.template.ts +22 -0
- package/src/cli/templates/repository.template.ts +42 -0
- package/src/cli/templates/service.template.ts +42 -0
- package/src/cli/utils/file-generator.ts +37 -0
- package/src/cli/utils/logger.ts +52 -0
- package/src/controllers/health.controller.ts +24 -0
- package/src/core/config/config-loader.ts +98 -0
- package/src/core/container/di-container.ts +43 -0
- package/src/core/decorators/auth-guard.decorator.ts +15 -0
- package/src/core/decorators/autowire.decorator.ts +18 -0
- package/src/core/decorators/controller.decorator.ts +15 -0
- package/src/core/decorators/injectable.decorator.ts +13 -0
- package/src/core/decorators/middleware.decorator.ts +18 -0
- package/src/core/decorators/repository.decorator.ts +6 -0
- package/src/core/decorators/route.decorator.ts +33 -0
- package/src/core/decorators/service.decorator.ts +6 -0
- package/src/core/openai/openai-client.ts +99 -0
- package/src/database/data-source.ts +29 -0
- package/src/entities/session.entity.ts +25 -0
- package/src/entities/user.entity.ts +27 -0
- package/src/middlewares/logging.middleware.ts +28 -0
- package/src/repositories/session.repository.ts +42 -0
- package/src/repositories/user.repository.ts +37 -0
- package/src/server.ts +32 -0
- package/src/services/health.service.ts +29 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
export function serviceTemplate(name: string): string {
|
|
3
|
+
const className = name.endsWith('Service') ? name : `${name}Service`;
|
|
4
|
+
const repoName = name.replace('Service', '') + 'Repository';
|
|
5
|
+
|
|
6
|
+
return `import { Service } from '../core/decorators/service.decorator';
|
|
7
|
+
import { Autowire } from '../core/decorators/autowire.decorator';
|
|
8
|
+
import { ${repoName} } from '../repositories/${name.toLowerCase()}.repository';
|
|
9
|
+
|
|
10
|
+
@Service()
|
|
11
|
+
export class ${className} {
|
|
12
|
+
constructor(
|
|
13
|
+
@Autowire() private ${repoName.charAt(0).toLowerCase() + repoName.slice(1)}: ${repoName}
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
async findAll() {
|
|
17
|
+
// Implement business logic
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async findById(id: number) {
|
|
22
|
+
// Implement business logic
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async create(data: any) {
|
|
27
|
+
// Implement business logic
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async update(id: number, data: any) {
|
|
32
|
+
// Implement business logic
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async delete(id: number) {
|
|
37
|
+
// Implement business logic
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export class FileGenerator {
|
|
5
|
+
static createDirectory(dirPath: string): void {
|
|
6
|
+
if (!fs.existsSync(dirPath)) {
|
|
7
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static writeFile(filePath: string, content: string): void {
|
|
12
|
+
const dir = path.dirname(filePath);
|
|
13
|
+
this.createDirectory(dir);
|
|
14
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static fileExists(filePath: string): boolean {
|
|
18
|
+
return fs.existsSync(filePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static readFile(filePath: string): string {
|
|
22
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static toCamelCase(str: string): string {
|
|
26
|
+
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static toPascalCase(str: string): string {
|
|
30
|
+
const camel = this.toCamelCase(str);
|
|
31
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static toKebabCase(str: string): string {
|
|
35
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora, { Ora } from 'ora';
|
|
3
|
+
|
|
4
|
+
export class CLILogger {
|
|
5
|
+
static success(message: string): void {
|
|
6
|
+
console.log(chalk.green('✓'), message);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static error(message: string): void {
|
|
10
|
+
console.log(chalk.red('✗'), message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static info(message: string): void {
|
|
14
|
+
console.log(chalk.blue('ℹ'), message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static warning(message: string): void {
|
|
18
|
+
console.log(chalk.yellow('⚠'), message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static title(message: string): void {
|
|
22
|
+
console.log(chalk.bold.cyan(`\n${message}\n`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static section(message: string): void {
|
|
26
|
+
console.log(chalk.bold(`\n${message}`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static spinner(text: string): Ora {
|
|
30
|
+
return ora(text).start();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static box(title: string, content: string[]): void {
|
|
34
|
+
const maxLength = Math.max(title.length, ...content.map(c => c.length)) + 4;
|
|
35
|
+
const border = '═'.repeat(maxLength);
|
|
36
|
+
|
|
37
|
+
console.log(chalk.cyan(`╔${border}╗`));
|
|
38
|
+
console.log(chalk.cyan('║') + chalk.bold(` ${title.padEnd(maxLength)} `) + chalk.cyan('║'));
|
|
39
|
+
console.log(chalk.cyan(`╠${border}╣`));
|
|
40
|
+
content.forEach(line => {
|
|
41
|
+
console.log(chalk.cyan('║') + ` ${line.padEnd(maxLength)} ` + chalk.cyan('║'));
|
|
42
|
+
});
|
|
43
|
+
console.log(chalk.cyan(`╚${border}╝`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static table(data: { [key: string]: string }): void {
|
|
47
|
+
const maxKeyLength = Math.max(...Object.keys(data).map(k => k.length));
|
|
48
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
49
|
+
console.log(` ${chalk.cyan(key.padEnd(maxKeyLength))} : ${value}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import { Controller } from '../core/decorators/controller.decorator';
|
|
3
|
+
import { Get } from '../core/decorators/route.decorator';
|
|
4
|
+
import { Injectable } from '../core/decorators/injectable.decorator';
|
|
5
|
+
import { Autowire } from '../core/decorators/autowire.decorator';
|
|
6
|
+
import { HealthService } from '../services/health.service';
|
|
7
|
+
|
|
8
|
+
@Controller('/api/health')
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class HealthController {
|
|
11
|
+
constructor(
|
|
12
|
+
@Autowire() private healthService: HealthService
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
@Get('/')
|
|
16
|
+
async getStatus() {
|
|
17
|
+
return await this.healthService.getStatus();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Get('/tip')
|
|
21
|
+
async getHealthTip() {
|
|
22
|
+
return await this.healthService.generateHealthTip();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
|
|
6
|
+
export interface AppConfig {
|
|
7
|
+
app: {
|
|
8
|
+
name: string;
|
|
9
|
+
port: number;
|
|
10
|
+
env: string;
|
|
11
|
+
};
|
|
12
|
+
database: {
|
|
13
|
+
type: string;
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
username: string;
|
|
17
|
+
password: string;
|
|
18
|
+
database: string;
|
|
19
|
+
synchronize: boolean;
|
|
20
|
+
logging: boolean;
|
|
21
|
+
};
|
|
22
|
+
openai: {
|
|
23
|
+
apiKey: string;
|
|
24
|
+
model: string;
|
|
25
|
+
};
|
|
26
|
+
auth: {
|
|
27
|
+
tokenExpiry: string;
|
|
28
|
+
sessionExpiry: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ConfigLoader {
|
|
33
|
+
private static config: AppConfig | null = null;
|
|
34
|
+
|
|
35
|
+
static load(useYaml: boolean = true): AppConfig {
|
|
36
|
+
if (this.config) {
|
|
37
|
+
return this.config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const configDir = path.join(process.cwd(), 'config');
|
|
41
|
+
let configPath: string;
|
|
42
|
+
|
|
43
|
+
if (useYaml) {
|
|
44
|
+
// Try .lock.yaml first, then .yaml
|
|
45
|
+
const lockPath = path.join(configDir, 'app.lock.yaml');
|
|
46
|
+
const yamlPath = path.join(configDir, 'app.yaml');
|
|
47
|
+
|
|
48
|
+
configPath = fs.existsSync(lockPath) ? lockPath : yamlPath;
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
throw new Error('Configuration file not found');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileContent = fs.readFileSync(configPath, 'utf8');
|
|
55
|
+
this.config = yaml.load(fileContent) as AppConfig;
|
|
56
|
+
} else {
|
|
57
|
+
// JSON fallback
|
|
58
|
+
configPath = path.join(configDir, 'app.json');
|
|
59
|
+
const fileContent = fs.readFileSync(configPath, 'utf8');
|
|
60
|
+
this.config = JSON.parse(fileContent);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Override with environment variables
|
|
64
|
+
this.config = this.mergeEnvVariables(this.config!);
|
|
65
|
+
|
|
66
|
+
return this.config;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static mergeEnvVariables(config: AppConfig): AppConfig {
|
|
70
|
+
return {
|
|
71
|
+
...config,
|
|
72
|
+
app: {
|
|
73
|
+
...config.app,
|
|
74
|
+
port: process.env.PORT ? parseInt(process.env.PORT) : config.app.port,
|
|
75
|
+
env: process.env.NODE_ENV || config.app.env
|
|
76
|
+
},
|
|
77
|
+
database: {
|
|
78
|
+
...config.database,
|
|
79
|
+
host: process.env.DB_HOST || config.database.host,
|
|
80
|
+
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : config.database.port,
|
|
81
|
+
username: process.env.DB_USERNAME || config.database.username,
|
|
82
|
+
password: process.env.DB_PASSWORD || config.database.password,
|
|
83
|
+
database: process.env.DB_DATABASE || config.database.database
|
|
84
|
+
},
|
|
85
|
+
openai: {
|
|
86
|
+
...config.openai,
|
|
87
|
+
apiKey: process.env.OPENAI_API_KEY || config.openai.apiKey
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static get(): AppConfig {
|
|
93
|
+
if (!this.config) {
|
|
94
|
+
throw new Error('Config not loaded. Call ConfigLoader.load() first.');
|
|
95
|
+
}
|
|
96
|
+
return this.config;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { AUTOWIRE_METADATA } from '../decorators/autowire.decorator';
|
|
4
|
+
|
|
5
|
+
export class DIContainer {
|
|
6
|
+
private static instances = new Map<any, any>();
|
|
7
|
+
private static classes = new Set<any>();
|
|
8
|
+
|
|
9
|
+
static register(target: any): void {
|
|
10
|
+
this.classes.add(target);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static resolve<T>(target: any): T {
|
|
14
|
+
// Return existing singleton if available
|
|
15
|
+
if (this.instances.has(target)) {
|
|
16
|
+
return this.instances.get(target);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get constructor parameter metadata
|
|
20
|
+
const params = Reflect.getOwnMetadata(AUTOWIRE_METADATA, target) || [];
|
|
21
|
+
const sortedParams = params.sort((a: any, b: any) => a.index - b.index);
|
|
22
|
+
|
|
23
|
+
// Resolve dependencies recursively
|
|
24
|
+
const dependencies = sortedParams.map((param: any) => {
|
|
25
|
+
return this.resolve(param.type);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Create instance
|
|
29
|
+
const instance = new target(...dependencies);
|
|
30
|
+
this.instances.set(target, instance);
|
|
31
|
+
|
|
32
|
+
return instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static getRegisteredClasses(): any[] {
|
|
36
|
+
return Array.from(this.classes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static clear(): void {
|
|
40
|
+
this.instances.clear();
|
|
41
|
+
this.classes.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
export const AUTH_GUARD_METADATA = 'auth:guard';
|
|
4
|
+
|
|
5
|
+
export function AuthGuard(): MethodDecorator & ClassDecorator {
|
|
6
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
|
|
7
|
+
if (propertyKey) {
|
|
8
|
+
// method decorator
|
|
9
|
+
Reflect.defineMetadata(AUTH_GUARD_METADATA, true, target.constructor, propertyKey);
|
|
10
|
+
} else {
|
|
11
|
+
// class decorator
|
|
12
|
+
Reflect.defineMetadata(AUTH_GUARD_METADATA, true, target);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
export const AUTOWIRE_METADATA = 'autowire:metadata';
|
|
5
|
+
|
|
6
|
+
export function Autowire(): ParameterDecorator {
|
|
7
|
+
return (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => {
|
|
8
|
+
const existingParams = Reflect.getOwnMetadata(AUTOWIRE_METADATA, target) || [];
|
|
9
|
+
const types = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
10
|
+
|
|
11
|
+
existingParams.push({
|
|
12
|
+
index: parameterIndex,
|
|
13
|
+
type: types[parameterIndex]
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
Reflect.defineMetadata(AUTOWIRE_METADATA, existingParams, target);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { DIContainer } from '../container/di-container';
|
|
4
|
+
|
|
5
|
+
export const CONTROLLER_METADATA = 'controller:metadata';
|
|
6
|
+
export const CONTROLLER_PATH = 'controller:path';
|
|
7
|
+
|
|
8
|
+
export function Controller(path: string = ''): ClassDecorator {
|
|
9
|
+
return (target: any) => {
|
|
10
|
+
Reflect.defineMetadata(CONTROLLER_METADATA, true, target);
|
|
11
|
+
Reflect.defineMetadata(CONTROLLER_PATH, path, target);
|
|
12
|
+
DIContainer.register(target);
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { DIContainer } from '../container/di-container';
|
|
4
|
+
|
|
5
|
+
export const INJECTABLE_METADATA = 'injectable:metadata';
|
|
6
|
+
|
|
7
|
+
export function Injectable(): ClassDecorator {
|
|
8
|
+
return (target: any) => {
|
|
9
|
+
Reflect.defineMetadata(INJECTABLE_METADATA, true, target);
|
|
10
|
+
DIContainer.register(target);
|
|
11
|
+
return target;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
export const MIDDLEWARE_METADATA = 'middleware:metadata';
|
|
5
|
+
|
|
6
|
+
export function Middleware(...middlewares: Function[]): ClassDecorator | MethodDecorator {
|
|
7
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
|
|
8
|
+
if (propertyKey) {
|
|
9
|
+
// Method-level middleware
|
|
10
|
+
const existing = Reflect.getOwnMetadata(MIDDLEWARE_METADATA, target.constructor, propertyKey) || [];
|
|
11
|
+
Reflect.defineMetadata(MIDDLEWARE_METADATA, [...existing, ...middlewares], target.constructor, propertyKey);
|
|
12
|
+
} else {
|
|
13
|
+
// Class-level middleware
|
|
14
|
+
const existing = Reflect.getOwnMetadata(MIDDLEWARE_METADATA, target) || [];
|
|
15
|
+
Reflect.defineMetadata(MIDDLEWARE_METADATA, [...existing, ...middlewares], target);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
export const ROUTE_METADATA = 'route:metadata';
|
|
5
|
+
|
|
6
|
+
export enum HttpMethod {
|
|
7
|
+
GET = 'get',
|
|
8
|
+
POST = 'post',
|
|
9
|
+
PUT = 'put',
|
|
10
|
+
DELETE = 'delete',
|
|
11
|
+
PATCH = 'patch'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createRouteDecorator(method: HttpMethod) {
|
|
15
|
+
return (path: string = ''): MethodDecorator => {
|
|
16
|
+
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
17
|
+
const routes = Reflect.getOwnMetadata(ROUTE_METADATA, target.constructor) || [];
|
|
18
|
+
routes.push({
|
|
19
|
+
method,
|
|
20
|
+
path,
|
|
21
|
+
handlerName: propertyKey
|
|
22
|
+
});
|
|
23
|
+
Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
|
|
24
|
+
return descriptor;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Get = createRouteDecorator(HttpMethod.GET);
|
|
30
|
+
export const Post = createRouteDecorator(HttpMethod.POST);
|
|
31
|
+
export const Put = createRouteDecorator(HttpMethod.PUT);
|
|
32
|
+
export const Delete = createRouteDecorator(HttpMethod.DELETE);
|
|
33
|
+
export const Patch = createRouteDecorator(HttpMethod.PATCH);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Injectable } from "../decorators/injectable.decorator";
|
|
2
|
+
import { ConfigLoader } from "../config/config-loader";
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class OpenAIClient {
|
|
6
|
+
private apiKey: string;
|
|
7
|
+
private model: string;
|
|
8
|
+
private baseURL: string = "https://api.openai.com/v1";
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
const config = ConfigLoader.get();
|
|
12
|
+
this.apiKey = config.openai.apiKey;
|
|
13
|
+
this.model = config.openai.model;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async complete(
|
|
17
|
+
prompt: string,
|
|
18
|
+
options?: {
|
|
19
|
+
model?: string;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
temperature?: number;
|
|
22
|
+
}
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
model: options?.model || this.model,
|
|
32
|
+
messages: [{ role: "user", content: prompt }],
|
|
33
|
+
max_tokens: options?.maxTokens || 1000,
|
|
34
|
+
temperature: options?.temperature || 0.7,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`OpenAI API error: ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = (await response.json()) as any;
|
|
43
|
+
return data.choices[0].message.content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async streamComplete(
|
|
47
|
+
prompt: string,
|
|
48
|
+
onChunk: (chunk: string) => void
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: this.model,
|
|
58
|
+
messages: [{ role: "user", content: prompt }],
|
|
59
|
+
stream: true,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`OpenAI API error: ${response.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const reader = response.body?.getReader();
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
|
|
70
|
+
if (!reader) {
|
|
71
|
+
throw new Error("No response body");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
|
|
78
|
+
const chunk = decoder.decode(value);
|
|
79
|
+
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
|
|
80
|
+
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.startsWith("data: ")) {
|
|
83
|
+
const data = line.slice(6);
|
|
84
|
+
if (data === "[DONE]") continue;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(data);
|
|
88
|
+
const content = parsed.choices[0]?.delta?.content;
|
|
89
|
+
if (content) {
|
|
90
|
+
onChunk(content);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Skip invalid JSON
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import { DataSource } from 'typeorm';
|
|
3
|
+
import { ConfigLoader } from '../core/config/config-loader';
|
|
4
|
+
import { User } from '../entities/user.entity';
|
|
5
|
+
import { Session } from '../entities/session.entity';
|
|
6
|
+
|
|
7
|
+
let AppDataSource: DataSource;
|
|
8
|
+
|
|
9
|
+
export function initializeDataSource(): DataSource {
|
|
10
|
+
const config = ConfigLoader.get();
|
|
11
|
+
|
|
12
|
+
AppDataSource = new DataSource({
|
|
13
|
+
type: config.database.type as any,
|
|
14
|
+
host: config.database.host,
|
|
15
|
+
port: config.database.port,
|
|
16
|
+
username: config.database.username,
|
|
17
|
+
password: config.database.password,
|
|
18
|
+
database: config.database.database,
|
|
19
|
+
synchronize: config.database.synchronize,
|
|
20
|
+
logging: config.database.logging,
|
|
21
|
+
entities: [User, Session],
|
|
22
|
+
migrations: [],
|
|
23
|
+
subscribers: []
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return AppDataSource;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { AppDataSource };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
|
3
|
+
import { User } from './user.entity';
|
|
4
|
+
|
|
5
|
+
@Entity('sessions')
|
|
6
|
+
export class Session {
|
|
7
|
+
@PrimaryGeneratedColumn()
|
|
8
|
+
id!: number;
|
|
9
|
+
|
|
10
|
+
@Column({ unique: true, length: 255 })
|
|
11
|
+
token!: string;
|
|
12
|
+
|
|
13
|
+
@Column()
|
|
14
|
+
userId!: number;
|
|
15
|
+
|
|
16
|
+
@ManyToOne(() => User, (user) => user.sessions, { onDelete: 'CASCADE' })
|
|
17
|
+
@JoinColumn({ name: 'userId' })
|
|
18
|
+
user!: User;
|
|
19
|
+
|
|
20
|
+
@Column()
|
|
21
|
+
expiresAt!: Date;
|
|
22
|
+
|
|
23
|
+
@CreateDateColumn()
|
|
24
|
+
createdAt!: Date;
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
|
3
|
+
import { Session } from './session.entity';
|
|
4
|
+
|
|
5
|
+
@Entity('users')
|
|
6
|
+
export class User {
|
|
7
|
+
@PrimaryGeneratedColumn()
|
|
8
|
+
id!: number;
|
|
9
|
+
|
|
10
|
+
@Column({ length: 100 })
|
|
11
|
+
name!: string;
|
|
12
|
+
|
|
13
|
+
@Column({ unique: true, length: 255 })
|
|
14
|
+
email!: string;
|
|
15
|
+
|
|
16
|
+
@Column({ length: 255 })
|
|
17
|
+
passwordHash!: string;
|
|
18
|
+
|
|
19
|
+
@OneToMany(() => Session, (session) => session.user)
|
|
20
|
+
sessions!: Session[];
|
|
21
|
+
|
|
22
|
+
@CreateDateColumn()
|
|
23
|
+
createdAt!: Date;
|
|
24
|
+
|
|
25
|
+
@UpdateDateColumn()
|
|
26
|
+
updatedAt!: Date;
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
import pino from 'pino';
|
|
4
|
+
|
|
5
|
+
const logger = pino({
|
|
6
|
+
transport: {
|
|
7
|
+
target: 'pino-pretty',
|
|
8
|
+
options: {
|
|
9
|
+
colorize: true
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
|
|
17
|
+
res.on('finish', () => {
|
|
18
|
+
const duration = Date.now() - start;
|
|
19
|
+
logger.info({
|
|
20
|
+
method: req.method,
|
|
21
|
+
url: req.url,
|
|
22
|
+
status: res.statusCode,
|
|
23
|
+
duration: `${duration}ms`
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
next();
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import { Repository as TypeORMRepository, MoreThan } from 'typeorm';
|
|
3
|
+
import { Repository } from '../core/decorators/repository.decorator';
|
|
4
|
+
import { Session } from '../entities/session.entity';
|
|
5
|
+
import { AppDataSource } from '../database/data-source';
|
|
6
|
+
|
|
7
|
+
@Repository()
|
|
8
|
+
export class SessionRepository {
|
|
9
|
+
private repository: TypeORMRepository<Session>;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.repository = AppDataSource.getRepository(Session);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findByToken(token: string): Promise<Session | null> {
|
|
16
|
+
return await this.repository.findOne({
|
|
17
|
+
where: {
|
|
18
|
+
token,
|
|
19
|
+
expiresAt: MoreThan(new Date())
|
|
20
|
+
},
|
|
21
|
+
relations: ['user']
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async create(sessionData: Partial<Session>): Promise<Session> {
|
|
26
|
+
const session = this.repository.create(sessionData);
|
|
27
|
+
return await this.repository.save(session);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async deleteByToken(token: string): Promise<boolean> {
|
|
31
|
+
const result = await this.repository.delete({ token });
|
|
32
|
+
return result.affected ? result.affected > 0 : false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async deleteExpired(): Promise<void> {
|
|
36
|
+
await this.repository
|
|
37
|
+
.createQueryBuilder()
|
|
38
|
+
.delete()
|
|
39
|
+
.where('expiresAt < :now', { now: new Date() })
|
|
40
|
+
.execute();
|
|
41
|
+
}
|
|
42
|
+
}
|