mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Project Initialization
This commit is contained in:
35
packages/api-common/package.json
Normal file
35
packages/api-common/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@edr/api-common",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edr/types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"@nestjs/common": "^10.4.6",
|
||||
"@nestjs/core": "^10.4.6",
|
||||
"@types/node": "^20.14.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
11
packages/api-common/src/decorators/current-user.decorator.ts
Normal file
11
packages/api-common/src/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
// TODO: integrate @edr/auth — this decorator currently returns whatever was
|
||||
// attached to request.user by the (not-yet-wired) auth middleware. Once the
|
||||
// auth package is integrated, replace this with the real user type.
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user ?? null;
|
||||
},
|
||||
);
|
||||
8
packages/api-common/src/decorators/public.decorator.ts
Normal file
8
packages/api-common/src/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
|
||||
// TODO: integrate @edr/auth — pair with the JwtAuthGuard from the auth package
|
||||
// so it skips authentication for routes marked @Public(). Until then, this is
|
||||
// just metadata.
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
8
packages/api-common/src/decorators/roles.decorator.ts
Normal file
8
packages/api-common/src/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
|
||||
// TODO: integrate @edr/auth — pair this metadata with a RolesGuard from the
|
||||
// auth package. Until then, this decorator only sets metadata and has no
|
||||
// runtime effect.
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
20
packages/api-common/src/entities/base.entity.ts
Normal file
20
packages/api-common/src/entities/base.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt?: Date | null;
|
||||
}
|
||||
58
packages/api-common/src/filters/http-exception.filter.ts
Normal file
58
packages/api-common/src/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
|
||||
interface ErrorResponseBody {
|
||||
success: false;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const messageRaw =
|
||||
exception instanceof HttpException ? exception.getResponse() : 'Internal server error';
|
||||
|
||||
const message =
|
||||
typeof messageRaw === 'string'
|
||||
? messageRaw
|
||||
: (messageRaw as { message?: string }).message ?? 'Unexpected error';
|
||||
|
||||
const body: ErrorResponseBody = {
|
||||
success: false,
|
||||
statusCode: status,
|
||||
message,
|
||||
error: exception instanceof Error ? exception.name : 'Error',
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
|
||||
if (status >= 500) {
|
||||
this.logger.error(`${request.method} ${request.url} -> ${status}`, (exception as Error)?.stack);
|
||||
} else {
|
||||
this.logger.warn(`${request.method} ${request.url} -> ${status} ${message}`);
|
||||
}
|
||||
|
||||
response.status(status).json(body);
|
||||
}
|
||||
}
|
||||
19
packages/api-common/src/index.ts
Normal file
19
packages/api-common/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Decorators
|
||||
export * from './decorators/current-user.decorator';
|
||||
export * from './decorators/roles.decorator';
|
||||
export * from './decorators/public.decorator';
|
||||
|
||||
// Filters
|
||||
export * from './filters/http-exception.filter';
|
||||
|
||||
// Interceptors
|
||||
export * from './interceptors/response-transform.interceptor';
|
||||
|
||||
// Pipes
|
||||
export * from './pipes/validation.pipe';
|
||||
|
||||
// Entities
|
||||
export * from './entities/base.entity';
|
||||
|
||||
// Repositories
|
||||
export * from './repositories/base.repository';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface StandardResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ResponseTransformInterceptor<T> implements NestInterceptor<T, StandardResponse<T>> {
|
||||
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<StandardResponse<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
packages/api-common/src/pipes/validation.pipe.ts
Normal file
10
packages/api-common/src/pipes/validation.pipe.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common';
|
||||
|
||||
export const createValidationPipe = (overrides?: ValidationPipeOptions): ValidationPipe =>
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: { enableImplicitConversion: true },
|
||||
...overrides,
|
||||
});
|
||||
52
packages/api-common/src/repositories/base.repository.ts
Normal file
52
packages/api-common/src/repositories/base.repository.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere,
|
||||
ObjectLiteral,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseRepository<T extends ObjectLiteral> {
|
||||
protected constructor(protected readonly repository: Repository<T>) {}
|
||||
|
||||
/** Find a single entity by its primary key. */
|
||||
async findById(id: string, options?: Omit<FindOneOptions<T>, 'where'>): Promise<T | null> {
|
||||
return this.repository.findOne({
|
||||
...options,
|
||||
where: { id } as unknown as FindOptionsWhere<T>,
|
||||
});
|
||||
}
|
||||
|
||||
/** Find many entities matching the given options. */
|
||||
async findAll(options?: FindManyOptions<T>): Promise<T[]> {
|
||||
return this.repository.find(options);
|
||||
}
|
||||
|
||||
/** Find many entities + return the total count for pagination. */
|
||||
async findAndCount(options?: FindManyOptions<T>): Promise<[T[], number]> {
|
||||
return this.repository.findAndCount(options);
|
||||
}
|
||||
|
||||
/** Create and persist a new entity. */
|
||||
async create(data: DeepPartial<T>): Promise<T> {
|
||||
const entity = this.repository.create(data);
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/** Patch an entity in place and return the reloaded row. */
|
||||
async update(id: string, data: DeepPartial<T>): Promise<T | null> {
|
||||
await this.repository.update(id, data as never);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/** Soft-delete an entity by primary key (sets deleted_at). */
|
||||
async softDelete(id: string): Promise<void> {
|
||||
await this.repository.softDelete(id);
|
||||
}
|
||||
|
||||
/** Permanently delete an entity. Avoid in domain code; prefer softDelete. */
|
||||
async hardDelete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
}
|
||||
9
packages/api-common/tsconfig.json
Normal file
9
packages/api-common/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/nestjs.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1
packages/api-common/tsconfig.tsbuildinfo
Normal file
1
packages/api-common/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user