Skip to content

Commit b898985

Browse files
Merge pull request #10 from nextinterfaces/refactor-service-items
Refactor service items and fix TS errors
2 parents 3125cb2 + 56eeb3a commit b898985

File tree

10 files changed

+592
-178
lines changed

10 files changed

+592
-178
lines changed

apps/items-service/bun.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"@opentelemetry/semantic-conventions": "^1.37.0",
1313
"postgres": "^3.4.4",
1414
},
15+
"devDependencies": {
16+
"bun-types": "^1.3.1",
17+
},
1518
},
1619
},
1720
"packages": {
@@ -213,6 +216,8 @@
213216

214217
"@types/pg-pool": ["@types/[email protected]", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="],
215218

219+
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
220+
216221
"@types/tedious": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
217222

218223
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -227,6 +232,8 @@
227232

228233
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
229234

235+
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
236+
230237
"cjs-module-lexer": ["[email protected]", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
231238

232239
"cliui": ["[email protected]", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -235,6 +242,8 @@
235242

236243
"color-name": ["[email protected]", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
237244

245+
"csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
246+
238247
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
239248

240249
"emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],

apps/items-service/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@
1515
"@opentelemetry/sdk-node": "^0.207.0",
1616
"@opentelemetry/semantic-conventions": "^1.37.0",
1717
"postgres": "^3.4.4"
18+
},
19+
"devDependencies": {
20+
"bun-types": "^1.3.1"
1821
}
1922
}

apps/items-service/src/config.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Configuration module
3+
* Centralizes all environment variable access and configuration management
4+
*/
5+
6+
export interface DatabaseConfig {
7+
host: string;
8+
port: number;
9+
username: string;
10+
password: string;
11+
database: string;
12+
max: number;
13+
idleTimeout: number;
14+
connectTimeout: number;
15+
}
16+
17+
export interface ServerConfig {
18+
port: number;
19+
apiPrefix: string;
20+
appPrefix: string;
21+
commitSha: string;
22+
}
23+
24+
export interface Config {
25+
server: ServerConfig;
26+
database: DatabaseConfig;
27+
}
28+
29+
/**
30+
* Load and validate configuration from environment variables
31+
*/
32+
export function loadConfig(): Config {
33+
return {
34+
server: {
35+
port: Number(process.env.PORT || 8080),
36+
apiPrefix: "/v1",
37+
appPrefix: "/items",
38+
commitSha: process.env.COMMIT_SHA || "unknown",
39+
},
40+
database: {
41+
host: process.env.DB_HOST || "localhost",
42+
port: Number(process.env.DB_PORT || 5432),
43+
username: process.env.DB_USER || "postgres",
44+
password: process.env.DB_PASSWORD || "postgres",
45+
database: process.env.DB_NAME || "postgres",
46+
max: 10,
47+
idleTimeout: 20,
48+
connectTimeout: 10,
49+
},
50+
};
51+
}
52+
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Controllers
3+
* Business logic for handling requests
4+
*/
5+
6+
import { trace, SpanStatusCode } from "@opentelemetry/api";
7+
import { ItemsRepository } from "./database.js";
8+
import type { CreateItemDto, HealthResponse, ItemsListResponse } from "./models.js";
9+
import { json, badRequest, internalServerError, serviceUnavailable } from "./http-utils.js";
10+
11+
/**
12+
* Health check controller
13+
*/
14+
export class HealthController {
15+
constructor(
16+
private repository: ItemsRepository,
17+
private commitSha: string
18+
) {}
19+
20+
async check(): Promise<Response> {
21+
const tracer = trace.getTracer("items-service");
22+
return await tracer.startActiveSpan("healthCheck", async (span) => {
23+
try {
24+
const isHealthy = await this.repository.healthCheck();
25+
26+
if (isHealthy) {
27+
const response: HealthResponse = {
28+
status: "ok",
29+
commit: this.commitSha,
30+
database: "connected",
31+
};
32+
span.setStatus({ code: SpanStatusCode.OK });
33+
return json(response);
34+
} else {
35+
const response: HealthResponse = {
36+
status: "degraded",
37+
commit: this.commitSha,
38+
database: "disconnected",
39+
};
40+
span.setStatus({ code: SpanStatusCode.ERROR, message: "Database disconnected" });
41+
return serviceUnavailable(response);
42+
}
43+
} catch (error) {
44+
span.recordException(error as Error);
45+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
46+
47+
const response: HealthResponse = {
48+
status: "degraded",
49+
commit: this.commitSha,
50+
database: "disconnected",
51+
error: String(error),
52+
};
53+
return serviceUnavailable(response);
54+
} finally {
55+
span.end();
56+
}
57+
});
58+
}
59+
}
60+
61+
/**
62+
* Items controller
63+
*/
64+
export class ItemsController {
65+
constructor(private repository: ItemsRepository) {}
66+
67+
/**
68+
* List all items
69+
*/
70+
async list(): Promise<Response> {
71+
const tracer = trace.getTracer("items-service");
72+
return await tracer.startActiveSpan("listItems", async (span) => {
73+
try {
74+
const items = await this.repository.findAll();
75+
span.setAttribute("items.count", items.length);
76+
span.setStatus({ code: SpanStatusCode.OK });
77+
78+
const response: ItemsListResponse = { items };
79+
return json(response);
80+
} catch (error) {
81+
console.error("Error fetching items:", error);
82+
span.recordException(error as Error);
83+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
84+
return internalServerError("Failed to fetch items");
85+
} finally {
86+
span.end();
87+
}
88+
});
89+
}
90+
91+
/**
92+
* Create a new item
93+
*/
94+
async create(req: Request): Promise<Response> {
95+
const tracer = trace.getTracer("items-service");
96+
return await tracer.startActiveSpan("createItem", async (span) => {
97+
try {
98+
// Parse and validate request body
99+
let body: Partial<CreateItemDto>;
100+
try {
101+
body = (await req.json()) as Partial<CreateItemDto>;
102+
} catch (error) {
103+
span.recordException(error as Error);
104+
span.setStatus({ code: SpanStatusCode.ERROR, message: "Invalid JSON" });
105+
span.end();
106+
return badRequest("invalid json");
107+
}
108+
109+
// Validate required fields
110+
if (!body?.name || typeof body.name !== "string") {
111+
span.setStatus({ code: SpanStatusCode.ERROR, message: "Name required" });
112+
span.end();
113+
return badRequest("name required");
114+
}
115+
116+
// Create item
117+
const item = await this.repository.create({ name: body.name });
118+
119+
span.setAttribute("item.id", item.id);
120+
span.setAttribute("item.name", item.name);
121+
span.setStatus({ code: SpanStatusCode.OK });
122+
123+
return json(item, { status: 201 });
124+
} catch (error) {
125+
console.error("Error creating item:", error);
126+
span.recordException(error as Error);
127+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
128+
return internalServerError("Failed to create item");
129+
} finally {
130+
span.end();
131+
}
132+
});
133+
}
134+
}
135+

apps/items-service/src/database.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import postgres from "postgres";
2+
import { trace, SpanStatusCode } from "@opentelemetry/api";
3+
import type { DatabaseConfig } from "./config.js";
4+
import type { Item, CreateItemDto } from "./models.js";
5+
6+
let sql: ReturnType<typeof postgres> | null = null;
7+
/**
8+
* Initialize database connection
9+
*/
10+
export function initDatabase(config: DatabaseConfig): ReturnType<typeof postgres> {
11+
if (sql) {
12+
return sql;
13+
}
14+
15+
sql = postgres({
16+
host: config.host,
17+
port: config.port,
18+
username: config.username,
19+
password: config.password,
20+
database: config.database,
21+
max: config.max,
22+
idle_timeout: config.idleTimeout,
23+
connect_timeout: config.connectTimeout,
24+
});
25+
26+
return sql;
27+
}
28+
29+
/**
30+
* Get the database connection
31+
*/
32+
export function getDatabase(): ReturnType<typeof postgres> {
33+
if (!sql) {
34+
throw new Error("Database not initialized. Call initDatabase first.");
35+
}
36+
return sql;
37+
}
38+
39+
/**
40+
* Initialize database schema
41+
*/
42+
export async function initSchema(): Promise<void> {
43+
const tracer = trace.getTracer("items-service");
44+
return await tracer.startActiveSpan("initDatabase", async (span) => {
45+
try {
46+
const db = getDatabase();
47+
await db`
48+
CREATE TABLE IF NOT EXISTS items (
49+
id SERIAL PRIMARY KEY,
50+
name VARCHAR(255) NOT NULL,
51+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
52+
)
53+
`;
54+
console.log("✅ Database initialized successfully");
55+
span.setStatus({ code: SpanStatusCode.OK });
56+
} catch (error) {
57+
console.error("❌ Failed to initialize database:", error);
58+
span.recordException(error as Error);
59+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
60+
throw error;
61+
} finally {
62+
span.end();
63+
}
64+
});
65+
}
66+
67+
/**
68+
* Repository for items data access
69+
*/
70+
export class ItemsRepository {
71+
private readonly db: ReturnType<typeof postgres>;
72+
73+
constructor() {
74+
this.db = getDatabase();
75+
}
76+
77+
/**
78+
* Get all items
79+
*/
80+
async findAll(): Promise<Item[]> {
81+
return await this.db<Item[]>`
82+
SELECT id, name
83+
FROM items
84+
ORDER BY id
85+
`;
86+
}
87+
88+
/**
89+
* Create a new item
90+
*/
91+
async create(dto: CreateItemDto): Promise<Item> {
92+
const [item] = await this.db<Item[]>`
93+
INSERT INTO items (name)
94+
VALUES (${dto.name})
95+
RETURNING id, name
96+
`;
97+
return item;
98+
}
99+
100+
/**
101+
* Check database connection health
102+
*/
103+
async healthCheck(): Promise<boolean> {
104+
try {
105+
await this.db`SELECT 1`;
106+
return true;
107+
} catch {
108+
return false;
109+
}
110+
}
111+
}
112+

0 commit comments

Comments
 (0)