Skip to content

Commit 6380406

Browse files
authored
Merge pull request #128 from JDIZM/refactor-response-error-handling
refactor: implement type-safe HTTP helpers and response patterns
2 parents af1be45 + 95fecb4 commit 6380406

23 files changed

+489
-401
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"workbench.colorTheme": "Darcula",
2+
"workbench.colorTheme": "Tokyo Night",
33
"workbench.iconTheme": "material-icon-theme",
44
"editor.fontFamily": "JetBrains Mono",
55
"editor.defaultFormatter": "esbenp.prettier-vscode",

scripts/dev-setup.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
* - Run seed first: `pnpm seed`
1313
* - Then add workspaces: `pnpm dev:workspace --email=alice@acmecorp.com --name="New Project"`
1414
*/
15-
import { db } from "@/services/db/drizzle.ts";
15+
import { logger } from "@/helpers/index.ts";
1616
import {
17+
accounts,
1718
profiles,
18-
workspaces,
1919
workspaceMemberships,
20-
accounts,
20+
workspaces,
2121
type AccountSelectType,
22-
type WorkspaceSelectType,
2322
type ProfileSelectType,
24-
type WorkspaceMembershipInsertType
23+
type WorkspaceMembershipInsertType,
24+
type WorkspaceSelectType
2525
} from "@/schema.ts";
26-
import { logger } from "@/helpers/index.ts";
26+
import { db } from "@/services/db/drizzle.ts";
2727
import { eq } from "drizzle-orm";
2828

2929
interface CreateWorkspaceOptions {

scripts/token-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import jwt from "jsonwebtoken";
2-
import dotenv from "dotenv";
31
import { logger } from "@/helpers/index.ts";
2+
import dotenv from "dotenv";
3+
import jwt from "jsonwebtoken";
44

55
dotenv.config();
66

src/docs/openapi-schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const ErrorResponseSchema = z
6767
})
6868
.openapi("ErrorResponse");
6969

70+
// TODO I think we need a filter model schema... maybe align with ag-grid? or create an adapter for ag-Grid.
7071
export const PaginationSchema = z
7172
.object({
7273
page: z.number().int().positive(),

src/handlers/accounts/accounts.handlers.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { HttpErrors, handleHttpError } from "@/helpers/HttpError.ts";
2-
import { gatewayResponse, logger } from "@/helpers/index.ts";
1+
import { HttpErrors, HttpStatusCode } from "@/helpers/Http.ts";
32
import { asyncHandler } from "@/helpers/request.ts";
3+
import { apiResponse } from "@/helpers/response.ts";
44
import { accounts, uuidSchema, type AccountSelectType, type AccountWithRelations } from "@/schema.ts";
55
import { db } from "@/services/db/drizzle.ts";
66
import type { Request, Response } from "express";
@@ -9,51 +9,54 @@ import { createDbAccount, getAccountWithRelations } from "./accounts.methods.ts"
99
export const getAccounts = asyncHandler(async (_req: Request, res: Response): Promise<void> => {
1010
const result = await db.select().from(accounts).execute();
1111

12-
logger.info({ msg: `Fetched accounts: ${result.length}` });
13-
14-
const response = gatewayResponse<AccountSelectType[]>().success(200, result);
12+
const response = apiResponse.success<AccountSelectType[]>(
13+
HttpStatusCode.OK,
14+
result,
15+
`Fetched accounts: ${result.length}`
16+
);
1517

1618
res.status(response.code).send(response);
1719
});
1820

1921
export const getAccount = asyncHandler(async (req: Request, res: Response): Promise<void> => {
2022
const validationResult = uuidSchema.safeParse({ uuid: req.params.id });
23+
2124
if (!validationResult.success) {
22-
handleHttpError(
23-
HttpErrors.ValidationFailed(`Invalid account ID: ${validationResult.error.message}`),
24-
res,
25-
gatewayResponse
25+
const response = apiResponse.error(
26+
HttpErrors.ValidationFailed(`Invalid account ID: ${validationResult.error.message}`)
2627
);
28+
res.status(response.code).send(response);
2729
return;
2830
}
2931

3032
if (!req.params.id) {
31-
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
33+
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
34+
res.status(response.code).send(response);
3235
return;
3336
}
3437

3538
const result = await getAccountWithRelations(req.params.id);
3639

3740
if (!result) {
38-
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
41+
const response = apiResponse.error(HttpErrors.NotFound("Account"));
42+
res.status(response.code).send(response);
3943
return;
4044
}
4145

42-
logger.info({ msg: `Fetched account with UUID ${req.params.id}` });
43-
44-
const response = gatewayResponse<AccountWithRelations>().success(200, result);
45-
46+
const response = apiResponse.success<AccountWithRelations>(
47+
HttpStatusCode.OK,
48+
result,
49+
`Fetched account with UUID ${req.params.id}`
50+
);
4651
res.status(response.code).send(response);
4752
});
4853

4954
export const createAccount = asyncHandler(async (req: Request, res: Response): Promise<void> => {
5055
const { fullName, phone, email } = req.body;
5156

52-
logger.info({ msg: `Creating account...` });
53-
5457
const accountId = await createDbAccount({ fullName, phone, email });
5558

56-
const response = gatewayResponse<string>().success(200, accountId);
59+
const response = apiResponse.success<string>(HttpStatusCode.OK, accountId, "Account created");
5760

5861
res.status(response.code).send(response);
5962
});

src/handlers/accounts/accounts.methods.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function getAccountById(accountId: string): Promise<AccountSelectTy
6363
return result;
6464
}
6565

66-
export async function getAccountWithRelations(accountId: string): Promise<AccountWithRelations> {
66+
export async function getAccountWithRelations(accountId: string): Promise<AccountWithRelations | null> {
6767
const validationResult = uuidSchema.safeParse({ uuid: accountId });
6868
if (!validationResult.success) {
6969
throw new Error(`Invalid account ID: ${validationResult.error.message}`);
@@ -85,9 +85,5 @@ export async function getAccountWithRelations(accountId: string): Promise<Accoun
8585
}
8686
});
8787

88-
if (!result) {
89-
throw new Error("Account not found");
90-
}
91-
92-
return result;
88+
return result || null;
9389
}

src/handlers/admin/admin.handlers.ts

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { HttpErrors, handleHttpError } from "@/helpers/HttpError.ts";
2-
import { gatewayResponse, logger } from "@/helpers/index.ts";
1+
import { HttpErrors, HttpStatusCode } from "@/helpers/Http.ts";
2+
import { apiResponse } from "@/helpers/response.ts";
33
import { asyncHandler } from "@/helpers/request.ts";
44
import { accounts, workspaceMemberships, workspaces } from "@/schema.ts";
55
import { AUDIT_ACTIONS, ENTITY_TYPES, auditHelpers, createAuditLog } from "@/services/auditLog.ts";
@@ -17,8 +17,6 @@ export const listAllAccounts = asyncHandler(async (req: Request, res: Response):
1717
const limit = parseInt(req.query.limit as string) || 20;
1818
const offset = (page - 1) * limit;
1919

20-
logger.info({ msg: `SuperAdmin listing all accounts - page: ${page}, limit: ${limit}` });
21-
2220
// Get paginated accounts with total count in single query
2321
const accountsWithCount = await db
2422
.select({
@@ -40,8 +38,8 @@ export const listAllAccounts = asyncHandler(async (req: Request, res: Response):
4038
// Clean data by removing totalCount from individual records
4139
const accountsList = accountsWithCount.map(({ totalCount: _, ...account }) => account);
4240

43-
const response = gatewayResponse().success(
44-
200,
41+
const response = apiResponse.success(
42+
HttpStatusCode.OK,
4543
{
4644
accounts: accountsList,
4745
pagination: {
@@ -66,18 +64,17 @@ export const createAccountForUser = asyncHandler(async (req: Request, res: Respo
6664
const { accountId } = req;
6765

6866
if (!accountId) {
69-
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
67+
const response = apiResponse.error(HttpErrors.Unauthorized());
68+
res.status(response.code).send(response);
7069
return;
7170
}
7271

73-
logger.info({ msg: `SuperAdmin creating account for ${email}` });
74-
7572
// Check if account already exists
7673
const [existingAccount] = await db.select().from(accounts).where(eq(accounts.email, email)).limit(1);
7774

7875
if (existingAccount) {
79-
logger.error({ email }, `Account with email ${email} already exists`);
80-
handleHttpError(HttpErrors.BadRequest("Unable to create account with provided information"), res, gatewayResponse);
76+
const response = apiResponse.error(HttpErrors.BadRequest("Unable to create account with provided information"));
77+
res.status(response.code).send(response);
8178
return;
8279
}
8380

@@ -121,11 +118,12 @@ export const createAccountForUser = asyncHandler(async (req: Request, res: Respo
121118
});
122119

123120
if (!newAccount) {
124-
handleHttpError(HttpErrors.DatabaseError("Failed to create account"), res, gatewayResponse);
121+
const response = apiResponse.error(HttpErrors.DatabaseError("Failed to create account"));
122+
res.status(response.code).send(response);
125123
return;
126124
}
127125

128-
const response = gatewayResponse().success(201, { account: newAccount }, "Account created successfully");
126+
const response = apiResponse.success(HttpStatusCode.CREATED, { account: newAccount }, "Account created successfully");
129127

130128
res.status(response.code).send(response);
131129
});
@@ -139,24 +137,25 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
139137
const { accountId } = req;
140138

141139
if (!targetAccountId) {
142-
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
140+
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
141+
res.status(response.code).send(response);
143142
return;
144143
}
145144

146145
if (!accountId) {
147-
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
146+
const response = apiResponse.error(HttpErrors.Unauthorized());
147+
res.status(response.code).send(response);
148148
return;
149149
}
150150

151151
const { isSuperAdmin } = req.body;
152152

153153
if (typeof isSuperAdmin !== "boolean") {
154-
handleHttpError(HttpErrors.ValidationFailed("isSuperAdmin must be a boolean value"), res, gatewayResponse);
154+
const response = apiResponse.error(HttpErrors.ValidationFailed("isSuperAdmin must be a boolean value"));
155+
res.status(response.code).send(response);
155156
return;
156157
}
157158

158-
logger.info({ msg: `SuperAdmin updating account ${targetAccountId} role to isSuperAdmin: ${isSuperAdmin}` });
159-
160159
// Get current account to track the change (outside transaction)
161160
const [currentAccount] = await db
162161
.select({ isSuperAdmin: accounts.isSuperAdmin })
@@ -165,7 +164,8 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
165164
.limit(1);
166165

167166
if (!currentAccount) {
168-
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
167+
const response = apiResponse.error(HttpErrors.NotFound("Account"));
168+
res.status(response.code).send(response);
169169
return;
170170
}
171171

@@ -195,12 +195,13 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
195195
});
196196

197197
if (!updatedAccount) {
198-
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
198+
const response = apiResponse.error(HttpErrors.NotFound("Account"));
199+
res.status(response.code).send(response);
199200
return;
200201
}
201202

202-
const response = gatewayResponse().success(
203-
200,
203+
const response = apiResponse.success(
204+
HttpStatusCode.OK,
204205
{ account: updatedAccount },
205206
`Account role updated to SuperAdmin: ${isSuperAdmin}`
206207
);
@@ -217,29 +218,28 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
217218
const { accountId } = req;
218219

219220
if (!targetAccountId) {
220-
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
221+
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
222+
res.status(response.code).send(response);
221223
return;
222224
}
223225

224226
if (!accountId) {
225-
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
227+
const response = apiResponse.error(HttpErrors.Unauthorized());
228+
res.status(response.code).send(response);
226229
return;
227230
}
228231

229232
const { status } = req.body;
230233
const validStatuses = ["active", "inactive", "suspended"];
231234

232235
if (!status || !validStatuses.includes(status)) {
233-
handleHttpError(
234-
HttpErrors.ValidationFailed(`Status must be one of: ${validStatuses.join(", ")}`),
235-
res,
236-
gatewayResponse
236+
const response = apiResponse.error(
237+
HttpErrors.ValidationFailed(`Status must be one of: ${validStatuses.join(", ")}`)
237238
);
239+
res.status(response.code).send(response);
238240
return;
239241
}
240242

241-
logger.info({ msg: `SuperAdmin updating account ${targetAccountId} status to: ${status}` });
242-
243243
// Get current account to track the change (outside transaction)
244244
const [currentAccount] = await db
245245
.select({ status: accounts.status })
@@ -248,7 +248,8 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
248248
.limit(1);
249249

250250
if (!currentAccount) {
251-
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
251+
const response = apiResponse.error(HttpErrors.NotFound("Account"));
252+
res.status(response.code).send(response);
252253
return;
253254
}
254255

@@ -267,11 +268,16 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
267268
});
268269

269270
if (!updatedAccount) {
270-
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
271+
const response = apiResponse.error(HttpErrors.NotFound("Account"));
272+
res.status(response.code).send(response);
271273
return;
272274
}
273275

274-
const response = gatewayResponse().success(200, { account: updatedAccount }, `Account status updated to: ${status}`);
276+
const response = apiResponse.success(
277+
HttpStatusCode.OK,
278+
{ account: updatedAccount },
279+
`Account status updated to: ${status}`
280+
);
275281

276282
res.status(response.code).send(response);
277283
});
@@ -285,8 +291,6 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
285291
const limit = parseInt(req.query.limit as string) || 20;
286292
const offset = (page - 1) * limit;
287293

288-
logger.info({ msg: `SuperAdmin listing all workspaces - page: ${page}, limit: ${limit}` });
289-
290294
// Get paginated workspaces with owner info, member counts, and total count in single query
291295
const workspacesWithCount = await db
292296
.select({
@@ -303,8 +307,8 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
303307
email: accounts.email
304308
},
305309
memberCount: sql<number>`(
306-
SELECT COUNT(*)
307-
FROM workspace_memberships
310+
SELECT COUNT(*)
311+
FROM workspace_memberships
308312
WHERE workspace_memberships.workspace_id = ${workspaces.uuid}
309313
)`,
310314
totalCount: sql<number>`count(*) over()`
@@ -319,8 +323,8 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
319323
// Clean data by removing totalCount from individual records
320324
const workspacesList = workspacesWithCount.map(({ totalCount: _, ...workspace }) => workspace);
321325

322-
const response = gatewayResponse().success(
323-
200,
326+
const response = apiResponse.success(
327+
HttpStatusCode.OK,
324328
{
325329
workspaces: workspacesList,
326330
pagination: {
@@ -349,8 +353,6 @@ export const listAllMemberships = asyncHandler(async (req: Request, res: Respons
349353
const workspaceId = req.query.workspaceId as string;
350354
const accountId = req.query.accountId as string;
351355

352-
logger.info({ msg: `SuperAdmin listing memberships - filters: workspace=${workspaceId}, account=${accountId}` });
353-
354356
// Build conditions for filtering
355357
const conditions = [];
356358
if (workspaceId) {
@@ -401,8 +403,8 @@ export const listAllMemberships = asyncHandler(async (req: Request, res: Respons
401403
const [countResult] = await countQuery;
402404
const count = countResult?.count || 0;
403405

404-
const response = gatewayResponse().success(
405-
200,
406+
const response = apiResponse.success(
407+
HttpStatusCode.OK,
406408
{
407409
memberships: membershipsList,
408410
pagination: {

0 commit comments

Comments
 (0)