fix(#1): Address code review findings
- Convert ApiResponse to discriminated union for type-safe error handling - Add HealthStatus type with HealthState literal union - Make BaseEntity fields readonly for immutability - Add GlobalExceptionFilter with structured logging - Add port validation with clear error messages in main.ts - Improve parseDate to log warnings for invalid dates - Add comprehensive Button tests (variants, onClick, disabled) - Add slugify edge case tests (empty, special chars, numbers) - Create ESLint configs for all packages - Remove compiled JS files from src directories - Convert .prettierrc.js to .prettierrc.json Refs #1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
14
apps/api/src/app.controller.d.ts
vendored
14
apps/api/src/app.controller.d.ts
vendored
@@ -1,14 +0,0 @@
|
||||
import { AppService } from "./app.service";
|
||||
import type { ApiResponse } from "@mosaic/shared/types";
|
||||
interface HealthStatus {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
export declare class AppController {
|
||||
private readonly appService;
|
||||
constructor(appService: AppService);
|
||||
getHello(): string;
|
||||
getHealth(): ApiResponse<HealthStatus>;
|
||||
}
|
||||
export {};
|
||||
//# sourceMappingURL=app.controller.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.controller.d.ts","sourceRoot":"","sources":["app.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBACa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,UAAU;IAGnD,QAAQ,IAAI,MAAM;IAKlB,SAAS,IAAI,WAAW,CAAC,YAAY,CAAC;CASvC"}
|
||||
@@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppController = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const app_service_1 = require("./app.service");
|
||||
let AppController = class AppController {
|
||||
appService;
|
||||
constructor(appService) {
|
||||
this.appService = appService;
|
||||
}
|
||||
getHello() {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
getHealth() {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.AppController = AppController;
|
||||
__decorate([
|
||||
(0, common_1.Get)(),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", String)
|
||||
], AppController.prototype, "getHello", null);
|
||||
__decorate([
|
||||
(0, common_1.Get)("health"),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", Object)
|
||||
], AppController.prototype, "getHealth", null);
|
||||
exports.AppController = AppController = __decorate([
|
||||
(0, common_1.Controller)(),
|
||||
__metadata("design:paramtypes", [app_service_1.AppService])
|
||||
], AppController);
|
||||
//# sourceMappingURL=app.controller.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,+CAA2C;AASpC,IAAM,aAAa,GAAnB,MAAM,aAAa;IACK;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAGvD,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;IAGD,SAAS;QACP,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AAlBY,sCAAa;AAIxB;IADC,IAAA,YAAG,GAAE;;;;6CAGL;AAGD;IADC,IAAA,YAAG,EAAC,QAAQ,CAAC;;;;8CASb;wBAjBU,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAE8B,wBAAU;GADxC,aAAa,CAkBzB"}
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { AppService } from "./app.service";
|
||||
import type { ApiResponse } from "@mosaic/shared";
|
||||
|
||||
interface HealthStatus {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
import type { ApiResponse, HealthStatus } from "@mosaic/shared";
|
||||
import { successResponse } from "@mosaic/shared";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -18,12 +14,9 @@ export class AppController {
|
||||
|
||||
@Get("health")
|
||||
getHealth(): ApiResponse<HealthStatus> {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
return successResponse({
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/api/src/app.module.d.ts
vendored
3
apps/api/src/app.module.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
//# sourceMappingURL=app.module.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.module.d.ts","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":"AAIA,qBAKa,SAAS;CAAG"}
|
||||
@@ -1,23 +0,0 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppModule = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const app_controller_1 = require("./app.controller");
|
||||
const app_service_1 = require("./app.service");
|
||||
let AppModule = class AppModule {
|
||||
};
|
||||
exports.AppModule = AppModule;
|
||||
exports.AppModule = AppModule = __decorate([
|
||||
(0, common_1.Module)({
|
||||
imports: [],
|
||||
controllers: [app_controller_1.AppController],
|
||||
providers: [app_service_1.AppService],
|
||||
})
|
||||
], AppModule);
|
||||
//# sourceMappingURL=app.module.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAOpC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IALrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,CAAC;KACxB,CAAC;GACW,SAAS,CAAG"}
|
||||
4
apps/api/src/app.service.d.ts
vendored
4
apps/api/src/app.service.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
export declare class AppService {
|
||||
getHello(): string;
|
||||
}
|
||||
//# sourceMappingURL=app.service.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.service.d.ts","sourceRoot":"","sources":["app.service.ts"],"names":[],"mappings":"AAEA,qBACa,UAAU;IACrB,QAAQ,IAAI,MAAM;CAGnB"}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
let AppService = class AppService {
|
||||
getHello() {
|
||||
return "Mosaic Stack API";
|
||||
}
|
||||
};
|
||||
exports.AppService = AppService;
|
||||
exports.AppService = AppService = __decorate([
|
||||
(0, common_1.Injectable)()
|
||||
], AppService);
|
||||
//# sourceMappingURL=app.service.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"app.service.js","sourceRoot":"","sources":["app.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAGrC,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,QAAQ;QACN,OAAO,kBAAkB,CAAC;IAC5B,CAAC;CACF,CAAA;AAJY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;GACA,UAAU,CAItB"}
|
||||
69
apps/api/src/filters/global-exception.filter.ts
Normal file
69
apps/api/src/filters/global-exception.filter.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
interface ErrorResponse {
|
||||
success: false;
|
||||
message: string;
|
||||
errorId: string;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const errorId = randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = "An unexpected error occurred";
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
message =
|
||||
typeof exceptionResponse === "string"
|
||||
? exceptionResponse
|
||||
: ((exceptionResponse as { message?: string }).message ?? message);
|
||||
} else if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
// Structured error logging
|
||||
const logPayload = {
|
||||
level: "error",
|
||||
errorId,
|
||||
timestamp,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
statusCode: status,
|
||||
message: exception instanceof Error ? exception.message : String(exception),
|
||||
stack: !isProduction && exception instanceof Error ? exception.stack : undefined,
|
||||
};
|
||||
|
||||
console.error(isProduction ? JSON.stringify(logPayload) : logPayload);
|
||||
|
||||
// Sanitized client response
|
||||
const errorResponse: ErrorResponse = {
|
||||
success: false,
|
||||
message:
|
||||
isProduction && status === HttpStatus.INTERNAL_SERVER_ERROR
|
||||
? "An unexpected error occurred"
|
||||
: message,
|
||||
errorId,
|
||||
timestamp,
|
||||
path: request.url,
|
||||
statusCode: status,
|
||||
};
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
2
apps/api/src/main.d.ts
vendored
2
apps/api/src/main.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=main.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":""}
|
||||
@@ -1,16 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core_1 = require("@nestjs/core");
|
||||
const app_module_1 = require("./app.module");
|
||||
async function bootstrap() {
|
||||
const app = await core_1.NestFactory.create(app_module_1.AppModule);
|
||||
app.enableCors();
|
||||
const port = process.env["PORT"] ?? 3001;
|
||||
await app.listen(port);
|
||||
console.log(`API running on http://localhost:${port}`);
|
||||
}
|
||||
bootstrap().catch((err) => {
|
||||
console.error("Failed to start application:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=main.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAChD,GAAG,CAAC,UAAU,EAAE,CAAC;IAEjB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;IACzC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEvB,OAAO,CAAC,GAAG,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACjC,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
||||
@@ -1,17 +1,61 @@
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
import { GlobalExceptionFilter } from "./filters/global-exception.filter";
|
||||
|
||||
function getPort(): number {
|
||||
const portEnv = process.env.PORT;
|
||||
|
||||
if (portEnv === undefined || portEnv === "") {
|
||||
return 3001;
|
||||
}
|
||||
|
||||
const port = parseInt(portEnv, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
throw new Error(`Invalid PORT environment variable: "${portEnv}". PORT must be a number.`);
|
||||
}
|
||||
|
||||
if (port < 1 || port > 65535) {
|
||||
throw new Error(
|
||||
`Invalid PORT environment variable: ${String(port)}. PORT must be between 1 and 65535.`
|
||||
);
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useGlobalFilters(new GlobalExceptionFilter());
|
||||
app.enableCors();
|
||||
|
||||
const port = process.env["PORT"] ?? 3001;
|
||||
const port = getPort();
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`API running on http://localhost:${port}`);
|
||||
console.log(`API running on http://localhost:${String(port)}`);
|
||||
}
|
||||
|
||||
bootstrap().catch((err: unknown) => {
|
||||
console.error("Failed to start application:", err);
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorStack = err instanceof Error ? err.stack : undefined;
|
||||
|
||||
if (isProduction) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
level: "error",
|
||||
message: "Failed to start application",
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to start application:", errorMessage);
|
||||
if (errorStack) {
|
||||
console.error(errorStack);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user