@mustafahmed1997/learnify-backend 1.9.0 → 2.0.1

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/README.md CHANGED
@@ -1,17 +1,54 @@
1
1
  # @mustafahmed1997/learnify-backend
2
2
 
3
- Shared backend utilities for Learnify microservices: Clerk-based auth/role
4
- middlewares, Zod-based request validation, error handling utilities, response
5
- shaping helpers, and the Express `Request` type augmentation for `req.user` —
6
- all importable from the package root.
7
-
8
- ## What this package solves
9
-
10
- Every Learnify microservice needs the same handful of cross-cutting pieces:
11
- who is the caller (Clerk auth), are they allowed to do this (role checks),
12
- is their request well-formed (Zod validation), and what shape should errors
13
- and responses take. This package centralizes all of that so each service
14
- doesn't reimplement or subtly diverge on it.
3
+ Shared backend building blocks for the Learnify e-learning microservices:
4
+ Clerk-based authentication and role middleware, Zod request validation, a
5
+ consistent error model with an Express error handler, Prisma error mapping,
6
+ response/message helpers, and the typed `req.user` augmentation.
7
+
8
+ Every service needs to answer the same questions: who is calling, are they
9
+ allowed to do this, is the request well-formed, and what shape should
10
+ responses and errors take. This package answers them once, so services don't
11
+ diverge.
12
+
13
+ ## Contents
14
+
15
+ - [Features](#features)
16
+ - [Installation](#installation)
17
+ - [Requirements and compatibility](#requirements-and-compatibility)
18
+ - [Quick start](#quick-start)
19
+ - [API reference](#api-reference)
20
+ - [Middleware](#middleware)
21
+ - [Errors](#errors)
22
+ - [Response and message helpers](#response-and-message-helpers)
23
+ - [Role utilities and constants](#role-utilities-and-constants)
24
+ - [Types](#types)
25
+ - [Error handling](#error-handling)
26
+ - [Testing](#testing)
27
+ - [Build and development](#build-and-development)
28
+ - [Project structure](#project-structure)
29
+ - [Contributing](#contributing)
30
+ - [Publishing](#publishing)
31
+
32
+ ## Features
33
+
34
+ | Export | Kind | Use it to |
35
+ | --- | --- | --- |
36
+ | `checkAuth` | middleware | Reject unauthenticated requests (`401`) and attach `req.user` |
37
+ | `checkRole` | middleware factory | Authorize by Clerk roles with `Any` / `All` / `Exact` matching |
38
+ | `validationMiddleware` | middleware factory | Validate `body`, `params` and `query` with Zod and write parsed values back |
39
+ | `AppError` | class | Throw an operational error with an HTTP status code |
40
+ | `ValidationError` | class | Throw a `400` error with field-level details |
41
+ | `globalErrorHandler` | error middleware | Turn errors into consistent JSON responses |
42
+ | `handlePrismaError` | function | Map a Prisma error to `{ status, message }` |
43
+ | `handleCaughtError` | function | Forward any caught error to `next()` in the right shape |
44
+ | `getJsonResponse` | function | Build a response body that omits `undefined` fields |
45
+ | `getSuccessMsg`, `getErrorMsg` | functions | Build consistent success/error sentences |
46
+ | `requiredFieldMsg`, `minLengthMsg`, … | functions | Reuse standard validation messages in Zod schemas |
47
+ | `checkUnique`, `checkValidRoles` | functions | Validate role arrays outside the middleware |
48
+ | `validRoles` | constant | The canonical role list: `STUDENT`, `INSTRUCTOR`, `ADMIN` |
49
+ | `Role`, `ClerkUser`, `Location`, `ValidationErrorItem`, `JsonResponseOptions`, `CheckRoleOptions`, `RoleCheckMode` | types | Type your services against the same contracts |
50
+
51
+ Everything is exported from the package root. There are no subpath exports.
15
52
 
16
53
  ## Installation
17
54
 
@@ -19,516 +56,518 @@ doesn't reimplement or subtly diverge on it.
19
56
  npm install @mustafahmed1997/learnify-backend
20
57
  ```
21
58
 
22
- ### Peer dependencies
23
-
24
- This package expects the consuming service to provide its own `express`,
25
- `@clerk/express`, and `zod` installs (so there is a single shared instance of
26
- each at runtime, and Clerk's request-scoped auth context works correctly):
59
+ `express`, `@clerk/express` and `zod` are **peer dependencies**. Install them in
60
+ the consuming service so a single instance of each is shared at runtime (Clerk's
61
+ request auth context and Zod's schema classes depend on this):
27
62
 
28
63
  ```bash
29
64
  npm install express @clerk/express zod
30
65
  ```
31
66
 
32
- ## Setup: `clerkMiddleware()` before `checkAuth` / `checkRole`
67
+ | Peer dependency | Supported range |
68
+ | --- | --- |
69
+ | `express` | `^5.1.0` |
70
+ | `@clerk/express` | `^1.7.74` |
71
+ | `zod` | `^4.5.4` |
72
+
73
+ ## Requirements and compatibility
33
74
 
34
- Both `checkAuth` and `checkRole` read Clerk auth state via `getAuth(req)`,
35
- which requires `clerkMiddleware()` from `@clerk/express` to have already run
36
- on the request:
75
+ - **ESM only.** The package is `"type": "module"` and its `exports` map
76
+ provides only an `import` condition. Use `import`, not `require()`.
77
+ - **Express 5.** The middleware targets Express 5 (for example, it handles
78
+ Express 5's read-only `req.query` getter).
79
+ - **Node.js.** The package doesn't declare an `engines` field. Its peer
80
+ `@clerk/express` (1.7.x) requires Node.js `>= 18.17.0`.
81
+ - **TypeScript.** Declarations (`.d.ts`) are bundled; no `@types` package is
82
+ needed. Because entry points are resolved through `exports`, the consuming
83
+ project needs `moduleResolution` set to `node16`, `nodenext` or `bundler`.
84
+ The package itself compiles with TypeScript 5 to an `ES2020` target.
85
+
86
+ ## Quick start
37
87
 
38
88
  ```ts
39
89
  import express from "express";
40
90
  import { clerkMiddleware } from "@clerk/express";
91
+ import { z } from "zod";
41
92
  import {
93
+ AppError,
42
94
  checkAuth,
43
95
  checkRole,
44
- validationMiddleware,
96
+ getJsonResponse,
97
+ getSuccessMsg,
45
98
  globalErrorHandler,
99
+ handleCaughtError,
100
+ validationMiddleware,
46
101
  } from "@mustafahmed1997/learnify-backend";
102
+ import { prisma } from "./prisma.js"; // your service's Prisma client
47
103
 
48
104
  const app = express();
49
105
 
50
106
  app.use(express.json());
51
- app.use(clerkMiddleware());
52
- ```
53
-
54
- ## Public exports
107
+ app.use(clerkMiddleware()); // must run before checkAuth / checkRole
55
108
 
56
- | Export | Kind | Purpose |
57
- | --- | --- | --- |
58
- | `checkAuth` | middleware | Requires Clerk authentication, attaches `req.user` |
59
- | `checkRole` | middleware factory | Role-based authorization (`Any` / `All` / `Exact`) |
60
- | `validationMiddleware` | middleware factory | Validates `body` / `params` / `query` against Zod schemas |
61
- | `validRoles`, `Role` | const / type | The canonical list of roles and their type |
62
- | `AppError` | class | Base operational error carrying an HTTP status code |
63
- | `ValidationError` | class | `AppError` subclass carrying structured field errors |
64
- | `ValidationErrorItem` | type | Shape of one entry in `ValidationError.errors` |
65
- | `globalErrorHandler` | middleware | Express error handler; register last |
66
- | `handlePrismaError` | function | Maps a Prisma error to `{ status, message }` |
67
- | `getErrorMsg`, `getSuccessMsg` | function | Consistent error/success message builders |
68
- | `getJsonResponse`, `JsonResponseOptions` | function / type | Builds a sparse, consistent response body |
69
- | `checkUnique` | function | Checks an array has no duplicate values |
70
- | `checkValidRoles` | function | Checks every value in an array is a valid `Role` |
71
- | `requiredFieldMsg`, `minLengthMsg`, `maxLengthMsg`, `invalidSchemaFormatMsg`, `invalidEmailMsg`, `invalidPasswordMsg`, `invalidDateFormatMsg`, `futureDateRequiredMsg`, `invalidNumberMsg`, `positiveNumberMsg`, `invalidUrlMsg` | function | Reusable validation error message builders (pair well with custom Zod `.refine()`/`.check()` messages) |
72
- | `ClerkUser` | type | Shape of `req.user` |
73
- | `Location` | type | `"body" \| "params" \| "query"` |
74
- | `CheckRoleOptions`, `RoleCheckMode` | type | Options accepted by `checkRole` |
75
-
76
- Importing anything from the package root also loads the Express `Request`
77
- type augmentation (`req.user?: ClerkUser`), so no extra `express.d.ts` is
78
- needed in the consuming service.
79
-
80
- ## `checkAuth`
81
-
82
- Verifies the request is authenticated via Clerk and attaches a typed
83
- `req.user` (a `ClerkUser`) for downstream handlers.
109
+ const createCourseSchema = z.object({
110
+ title: z.string().trim().min(3),
111
+ price: z.number().positive(),
112
+ });
84
113
 
85
- ```ts
86
- import { checkAuth } from "@mustafahmed1997/learnify-backend";
114
+ app.post(
115
+ "/courses",
116
+ checkAuth,
117
+ checkRole(["INSTRUCTOR", "ADMIN"]),
118
+ validationMiddleware({ body: createCourseSchema }),
119
+ async (req, res, next) => {
120
+ try {
121
+ const course = await prisma.course.create({ data: req.body });
122
+ res.status(201).json(
123
+ getJsonResponse({
124
+ success: true,
125
+ message: getSuccessMsg("Course", "has", "created"),
126
+ data: course,
127
+ }),
128
+ );
129
+ } catch (error) {
130
+ handleCaughtError(error, next);
131
+ }
132
+ },
133
+ );
87
134
 
88
- app.get("/me", checkAuth, (req, res) => {
89
- res.json(req.user);
135
+ app.get("/courses/:id", async (req, res, next) => {
136
+ try {
137
+ const course = await prisma.course.findUnique({
138
+ where: { id: req.params.id },
139
+ });
140
+ if (!course) throw new AppError("Course not found", 404);
141
+ res.json(getJsonResponse({ success: true, data: course }));
142
+ } catch (error) {
143
+ handleCaughtError(error, next);
144
+ }
90
145
  });
91
- ```
92
146
 
93
- If the request is not authenticated, `checkAuth` responds `401` with:
94
-
95
- ```json
96
- { "error": "Unauthenticated. Login first please." }
147
+ app.use(globalErrorHandler); // register last
97
148
  ```
98
149
 
99
- and does not call `next()`. If reading the Clerk auth context throws, the
100
- error is passed to `next(error)` for `globalErrorHandler` to handle.
150
+ ## API reference
101
151
 
102
- ## `checkRole`
152
+ ### Middleware
103
153
 
104
- Authorizes a request based on the caller's roles in the Clerk session claims
105
- (`sessionClaims.userMetadata.roles`, an array). Takes an array of allowed
106
- roles and an optional `{ mode }`.
154
+ #### `checkAuth(req, res, next)`
107
155
 
108
- ```ts
109
- import { checkRole } from "@mustafahmed1997/learnify-backend";
156
+ Requires a Clerk-authenticated request and attaches a typed `req.user`.
157
+ `clerkMiddleware()` from `@clerk/express` must run first, because `checkAuth`
158
+ reads the auth state with `getAuth(req)`.
110
159
 
111
- app.delete(
112
- "/courses/:id",
113
- checkRole(["ADMIN", "INSTRUCTOR"]),
114
- deleteCourseController,
115
- );
160
+ ```ts
161
+ app.get("/me", checkAuth, (req, res) => {
162
+ res.json(req.user);
163
+ });
116
164
  ```
117
165
 
118
- ### Modes
166
+ | Situation | Result |
167
+ | --- | --- |
168
+ | Authenticated with a `userId` | Sets `req.user`, calls `next()` |
169
+ | Not authenticated, or no `userId` | Responds `401` `{ "error": "Unauthenticated. Login first please." }`; `next()` is not called |
170
+ | `getAuth` throws (e.g. `clerkMiddleware()` missing) | Calls `next(error)` → `500` from `globalErrorHandler` |
119
171
 
120
- `mode` defaults to `"Any"` when omitted.
172
+ `req.user` is built from the session token: `clerkId` comes from `userId`.
173
+ `email`, `firstName`, `lastName`, `imageURL` and `userMetadata` are read from
174
+ custom session claims of the same names. Add them to your Clerk session token
175
+ for them to be populated; missing claims become `undefined`.
121
176
 
122
- - **`Any`** — the caller needs at least one of `allowedRoles`. Extra roles on
123
- either side are ignored.
177
+ #### `checkRole(allowedRoles, options?)`
124
178
 
125
- ```ts
126
- // Passes if the caller has ADMIN or INSTRUCTOR (or both)
127
- checkRole(["ADMIN", "INSTRUCTOR"], { mode: "Any" });
128
- ```
179
+ Returns a middleware that authorizes the caller against the roles array at
180
+ `sessionClaims.userMetadata.roles`. Like `checkAuth`, it requires
181
+ `clerkMiddleware()` to have run.
129
182
 
130
- - **`All`** — the caller must have every role in `allowedRoles`, but may have
131
- additional roles beyond that.
132
-
133
- ```ts
134
- // Passes only if the caller has both ADMIN and INSTRUCTOR (extra roles OK)
135
- checkRole(["ADMIN", "INSTRUCTOR"], { mode: "All" });
136
- ```
137
-
138
- - **`Exact`** — the caller's role set must match `allowedRoles` exactly, with
139
- no missing or extra roles.
140
-
141
- ```ts
142
- // Passes only if the caller's roles are exactly {ADMIN, INSTRUCTOR}
143
- checkRole(["ADMIN", "INSTRUCTOR"], { mode: "Exact" });
144
- ```
183
+ ```ts
184
+ import { checkRole } from "@mustafahmed1997/learnify-backend";
145
185
 
146
- ### Responses
186
+ app.delete("/courses/:id", checkAuth, checkRole(["ADMIN"]), deleteCourse);
187
+ ```
147
188
 
148
- - Empty or duplicate `allowedRoles`, or duplicate/invalid roles coming back
149
- from Clerk's session claims, are treated as misconfiguration/upstream data
150
- problems: the middleware calls `next(error)` with a plain `Error`, which
151
- `globalErrorHandler` turns into a `500`.
152
- - No `sessionClaims`, no `userMetadata.roles`, or an empty roles array
153
- responds `403` with `{ "message": "User roles not found" }` and does not
154
- call `next()`.
155
- - A caller whose roles don't satisfy the selected mode responds `403` with
156
- `{ "message": "Forbidden" }` and does not call `next()`.
157
- - An authorized caller calls `next()` with no arguments.
189
+ | Parameter | Type | Description |
190
+ | --- | --- | --- |
191
+ | `allowedRoles` | `Role[]` | Non-empty array of unique roles |
192
+ | `options.mode` | `"Any" \| "All" \| "Exact"` | Matching strategy. Default `"Any"` |
158
193
 
159
- ### Combined route example
194
+ | Mode | Authorized when |
195
+ | --- | --- |
196
+ | `Any` | The user has at least one of `allowedRoles` |
197
+ | `All` | The user has every role in `allowedRoles` (extra roles allowed) |
198
+ | `Exact` | The user's roles are exactly `allowedRoles` (no missing or extra roles) |
160
199
 
161
200
  ```ts
162
- app.patch(
163
- "/users/:id/role",
164
- checkAuth,
165
- checkRole(["ADMIN"]),
166
- updateRoleController,
167
- );
201
+ checkRole(["ADMIN", "INSTRUCTOR"]); // either role
202
+ checkRole(["ADMIN", "INSTRUCTOR"], { mode: "All" }); // both roles, extras OK
203
+ checkRole(["ADMIN", "INSTRUCTOR"], { mode: "Exact" }); // exactly these two
168
204
  ```
169
205
 
170
- ## `validationMiddleware`
206
+ | Situation | Result |
207
+ | --- | --- |
208
+ | Authorized | Calls `next()` |
209
+ | No session claims, no `roles`, or empty `roles` | `403` `{ "message": "User roles not found" }` |
210
+ | Roles don't satisfy the mode (or an unknown mode is passed) | `403` `{ "message": "Forbidden" }` |
211
+ | `allowedRoles` empty or contains duplicates | `next(Error)` → `500` (misconfiguration) |
212
+ | Clerk roles contain duplicates or values outside `validRoles` | `next(Error)` → `500` (bad upstream data) |
171
213
 
172
- Validates `req.body`, `req.params`, and/or `req.query` against Zod schemas.
173
- Any subset of the three locations may be supplied — omitted locations are
174
- skipped.
214
+ #### `validationMiddleware(schemas)`
215
+
216
+ Returns a middleware that validates any combination of `body`, `params` and
217
+ `query` against Zod schemas. Omitted (or `undefined`) locations are skipped.
175
218
 
176
219
  ```ts
177
220
  import { z } from "zod";
178
221
  import { validationMiddleware } from "@mustafahmed1997/learnify-backend";
179
222
 
180
- const createCourseSchema = z.object({
181
- title: z.string().min(3),
182
- price: z.number().positive(),
183
- });
184
-
185
- const courseIdParamsSchema = z.object({
186
- id: z.string().uuid(),
187
- });
188
-
189
- const paginationQuerySchema = z.object({
190
- page: z.coerce.number().int().positive().default(1),
191
- });
192
-
193
- app.post(
194
- "/courses",
195
- validationMiddleware({ body: createCourseSchema }),
196
- createCourseController,
197
- );
198
-
199
- app.get(
200
- "/courses/:id",
201
- validationMiddleware({ params: courseIdParamsSchema }),
202
- getCourseController,
203
- );
204
-
205
223
  app.get(
206
- "/courses",
207
- validationMiddleware({ query: paginationQuerySchema }),
208
- listCoursesController,
209
- );
210
-
211
- // All three at once:
212
- app.patch(
213
- "/courses/:id",
224
+ "/courses/:id/lessons",
214
225
  validationMiddleware({
215
- params: courseIdParamsSchema,
216
- body: createCourseSchema.partial(),
217
- query: paginationQuerySchema,
226
+ params: z.object({ id: z.uuid() }),
227
+ query: z.object({ page: z.coerce.number().int().positive().default(1) }),
218
228
  }),
219
- updateCourseController,
229
+ (req, res) => {
230
+ // req.query.page is a number here, not a string
231
+ },
220
232
  );
221
233
  ```
222
234
 
223
- - On success, `validationMiddleware` calls `next()` with no arguments **and**
224
- writes each schema's parsed/transformed output back onto `req.body`,
225
- `req.params`, and/or `req.query`. This means transforms like
226
- `z.coerce.number()` or `.default(...)` are reflected in what the
227
- controller sees — e.g. `req.query.page` above is a `number`, not the raw
228
- query string.
229
- - On failure, it calls `next(validationError)` with a `ValidationError`
230
- (never touches `res` directly) collecting **every** failing field across
231
- **all** provided locations in one pass — it does not stop at the first
232
- invalid location.
233
- - Register `globalErrorHandler` after your routes to turn that
234
- `ValidationError` into the `400` response documented below.
235
+ - **On success** it calls `next()` and replaces `req.body`, `req.params` and
236
+ `req.query` with each schema's parsed output. Coercions, defaults,
237
+ transforms and stripped unknown keys are all visible to the handler.
238
+ - **On failure** it calls `next(new ValidationError("Validation failed", errors))`.
239
+ It collects every issue from every location in one pass and never writes
240
+ to `res` itself. Invalid locations are left untouched on `req`.
241
+ - Each error item is `{ location, field, message }`. `field` is the Zod issue
242
+ path joined with `.` (e.g. `address.city`, `tags.1`), or `""` when the
243
+ whole location is invalid (e.g. a missing body).
235
244
 
236
- ## Error classes
245
+ > TypeScript note: `req.body`, `req.params` and `req.query` keep their Express
246
+ > types after validation. Cast or re-parse in the handler if you need the
247
+ > schema's inferred type.
237
248
 
238
- ### `AppError`
249
+ ### Errors
239
250
 
240
- A typed operational error carrying an HTTP status code.
251
+ #### `AppError`
241
252
 
242
253
  ```ts
243
- import { AppError } from "@mustafahmed1997/learnify-backend";
244
-
245
- if (!course) {
246
- throw new AppError("Course not found", 404);
247
- }
254
+ new AppError(message: string, statusCode: number)
248
255
  ```
249
256
 
250
- ### `ValidationError`
251
-
252
- An `AppError` subclass (status code always `400`) that also carries the list
253
- of field-level failures. This is what `validationMiddleware` throws, but you
254
- can also construct and throw it directly for manual validation:
257
+ An `Error` subclass with a `statusCode`. Its prototype chain is preserved, so
258
+ `instanceof AppError` works for subclasses too.
255
259
 
256
260
  ```ts
257
- import { ValidationError } from "@mustafahmed1997/learnify-backend";
258
- import type { ValidationErrorItem } from "@mustafahmed1997/learnify-backend";
261
+ import { AppError } from "@mustafahmed1997/learnify-backend";
259
262
 
260
- const errors: ValidationErrorItem[] = [
261
- { location: "body", field: "role", message: "Invalid role" },
262
- ];
263
+ throw new AppError("Course not found", 404);
263
264
 
264
- throw new ValidationError("Validation failed", errors);
265
+ class ConflictError extends AppError {
266
+ constructor(message: string) {
267
+ super(message, 409);
268
+ }
269
+ }
265
270
  ```
266
271
 
267
- `ValidationErrorItem` shape:
272
+ #### `ValidationError`
268
273
 
269
274
  ```ts
270
- type ValidationErrorItem = {
271
- location: "body" | "params" | "query";
272
- field: string;
273
- message: string;
274
- };
275
+ new ValidationError(message: string, errors: ValidationErrorItem[])
275
276
  ```
276
277
 
277
- ## `globalErrorHandler`
278
-
279
- Express error-handling middleware. Register it **last**, after all routes:
278
+ An `AppError` whose `statusCode` is always `400`, carrying field-level
279
+ details in `errors`. `validationMiddleware` produces these, and you can throw
280
+ one yourself:
280
281
 
281
282
  ```ts
282
- import { globalErrorHandler } from "@mustafahmed1997/learnify-backend";
283
+ import { ValidationError } from "@mustafahmed1997/learnify-backend";
283
284
 
284
- app.use(globalErrorHandler);
285
+ throw new ValidationError("Validation failed", [
286
+ { location: "body", field: "email", message: "Email already in use" },
287
+ ]);
285
288
  ```
286
289
 
287
- Behavior, checked in this order:
290
+ #### `globalErrorHandler(err, req, res, next)`
288
291
 
289
- 1. **`ValidationError`** (including whatever `validationMiddleware` throws)
290
- → `400` with the field-level errors:
292
+ Express error-handling middleware. Register it after all routes. See
293
+ [Error handling](#error-handling) for its response contract.
291
294
 
292
- ```json
293
- {
294
- "error": "Validation failed",
295
- "errors": [
296
- { "location": "body", "field": "role", "message": "Invalid role" },
297
- { "location": "params", "field": "userId", "message": "Invalid user ID" }
298
- ]
299
- }
300
- ```
295
+ #### `handlePrismaError(error)`
301
296
 
302
- 2. Any other **`AppError`** → its own `statusCode` with `{ "error": err.message }`.
303
- 3. Anything else (an unexpected `Error`, a rejected promise with a non-Error
304
- value, etc.) → `500` with a fixed, generic body that never leaks the
305
- underlying error's message or stack:
306
-
307
- ```json
308
- { "error": "Internal server error" }
309
- ```
297
+ ```ts
298
+ handlePrismaError(error: unknown): { status: number; message: string }
299
+ ```
310
300
 
311
- The original error is still logged via `console.error` for observability
312
- — it just isn't sent to the client.
301
+ Maps a Prisma error to an HTTP status and message. It never throws, and it
302
+ doesn't import `@prisma/client`; it inspects the error's shape:
313
303
 
314
- ## `handlePrismaError`
304
+ - A **known request error** is any object whose `code` is a string matching
305
+ `P` + four digits (e.g. `PrismaClientKnownRequestError`).
306
+ - A **validation error** is any object whose `name` is
307
+ `"PrismaClientValidationError"`.
315
308
 
316
- Maps a Prisma error (by `error.code` / `error.name`) to a plain
317
- `{ status, message }` pair, for services that want to translate Prisma
318
- failures into an `AppError` themselves:
309
+ | Input | `status` | `message` |
310
+ | --- | --- | --- |
311
+ | `P2002` unique constraint | `409` | `Duplicate value for <meta.target>` (falls back to `field`; arrays are comma-joined) |
312
+ | `P2003` foreign key | `400` | `Invalid foreign key reference` |
313
+ | `P2014` relation violation | `400` | `Invalid relation reference` |
314
+ | `P2025` record not found | `404` | `Record not found` |
315
+ | `P2000` value too long | `400` | `Value too long for <meta.column_name>` (falls back to `column`) |
316
+ | Any other `P####` code | `500` | `Database error: <error.message>` |
317
+ | `PrismaClientValidationError` | `400` | `Invalid input data: <error.message>` |
318
+ | Anything else (incl. `null`, strings) | `500` | `Non-prisma error: <message or String(value)>` |
319
319
 
320
320
  ```ts
321
- import { handlePrismaError, AppError } from "@mustafahmed1997/learnify-backend";
322
-
323
- try {
324
- await prisma.course.create({ data });
325
- } catch (err) {
326
- const { status, message } = handlePrismaError(err);
327
- throw new AppError(message, status);
328
- }
321
+ const { status, message } = handlePrismaError(err);
329
322
  ```
330
323
 
331
- Handles `P2002` (unique constraint), `P2003` (invalid foreign key), `P2025`
332
- (record not found), `P2014` (invalid relation), `P2000` (value too long),
333
- `PrismaClientValidationError`, and falls back to a generic `500` message for
334
- anything else — including `null`/`undefined`/non-object inputs.
335
-
336
- ## `validRoles` and `Role`
324
+ #### `handleCaughtError(error, next)`
337
325
 
338
326
  ```ts
339
- import { validRoles } from "@mustafahmed1997/learnify-backend";
340
- import type { Role } from "@mustafahmed1997/learnify-backend";
341
-
342
- console.log(validRoles); // ["STUDENT", "INSTRUCTOR", "ADMIN"]
343
-
344
- function isValidRole(value: string): value is Role {
345
- return (validRoles as readonly string[]).includes(value);
346
- }
327
+ handleCaughtError(error: unknown, next: NextFunction): void
347
328
  ```
348
329
 
349
- ## Role-checking utilities
350
-
351
- Low-level building blocks used internally by `checkRole`, exported for
352
- services that need custom role-array validation outside of the middleware:
330
+ The recommended way to forward an error from a `catch` block. It decides what
331
+ reaches `globalErrorHandler`:
353
332
 
354
- ```ts
355
- import { checkUnique, checkValidRoles } from "@mustafahmed1997/learnify-backend";
333
+ | Caught error | Passed to `next()` | Client sees |
334
+ | --- | --- | --- |
335
+ | `AppError` / `ValidationError` | The same error, unchanged | Its status and message |
336
+ | Prisma error mapped to a 4xx (`P2000`, `P2002`, `P2003`, `P2014`, `P2025`, validation) | `new AppError(message, status)` | e.g. `404 Record not found` |
337
+ | Anything mapped to `500` (other Prisma codes, non-Prisma errors, non-`Error` values) | The original value | Generic `500 Internal server error` |
356
338
 
357
- checkUnique(["ADMIN", "ADMIN"]); // false
358
- checkUnique(["ADMIN", "STUDENT"]); // true
339
+ Server errors are forwarded untouched on purpose. Their real messages are
340
+ logged by `globalErrorHandler` and never sent to clients.
359
341
 
360
- checkValidRoles(["ADMIN", "STUDENT"]); // true
361
- checkValidRoles(["ADMIN", "SUPERADMIN"]); // false — not in validRoles
362
- ```
342
+ ### Response and message helpers
363
343
 
364
- ## Response utilities
344
+ #### `getJsonResponse(options)`
365
345
 
366
- ### `getJsonResponse`
346
+ ```ts
347
+ getJsonResponse<TData, TAdditionalInfo>({
348
+ success: boolean; // required, always included
349
+ message?: string;
350
+ data?: TData;
351
+ additionalInfo?: TAdditionalInfo;
352
+ error?: string;
353
+ })
354
+ ```
367
355
 
368
- Builds a response body containing only the fields you actually pass —
369
- `undefined` fields are omitted rather than serialized:
356
+ Builds a response body that contains `success` plus only the optional fields
357
+ that are not `undefined`. Falsy values such as `0`, `false`, `""` and `null`
358
+ are kept.
370
359
 
371
360
  ```ts
372
- import { getJsonResponse } from "@mustafahmed1997/learnify-backend";
373
-
374
- res.status(200).json(
361
+ res.json(
375
362
  getJsonResponse({
376
- message: "Course created",
377
- data: course,
363
+ success: true,
364
+ data: courses,
365
+ additionalInfo: { page: 1, total: 42 },
378
366
  }),
379
367
  );
380
- // { "message": "Course created", "data": { ... } }
368
+ // { "success": true, "data": [...], "additionalInfo": { "page": 1, "total": 42 } }
381
369
  ```
382
370
 
383
- ### `getErrorMsg` / `getSuccessMsg`
384
-
385
- Small helpers for consistent, grammatically-correct singular/plural messages:
371
+ #### `getSuccessMsg(resource, verb, action)` / `getErrorMsg(resource, verb, message)`
386
372
 
387
373
  ```ts
388
- import { getErrorMsg, getSuccessMsg } from "@mustafahmed1997/learnify-backend";
389
-
390
- getErrorMsg("Course", "was", "not found"); // "Course was not found"
374
+ getSuccessMsg("Course", "has", "created"); // "Course has been created successfully"
375
+ getSuccessMsg("Courses", "have", "deleted"); // "Courses have been deleted successfully"
376
+ getErrorMsg("Course", "was", "not found"); // "Course was not found"
391
377
  getErrorMsg("Courses", "were", "not found"); // "Courses were not found"
392
-
393
- getSuccessMsg("Course", "has", "created"); // "Course has been created successfully"
394
- getSuccessMsg("Courses", "have", "created"); // "Courses have been created successfully"
395
378
  ```
396
379
 
397
- ## Validation error message utilities
398
-
399
- Reusable message builders for custom Zod validators (e.g. inside
400
- `.refine()`), so wording stays consistent across services:
380
+ `verb` is typed as `"has" | "have"` and `"was" | "were"` respectively. The
381
+ resource is not pluralized automatically.
382
+
383
+ #### Validation message builders
384
+
385
+ | Function | Returns |
386
+ | --- | --- |
387
+ | `requiredFieldMsg(field)` | `"<field> is required"` |
388
+ | `minLengthMsg(min)` | `"Minimum length is <min>"` |
389
+ | `maxLengthMsg(max)` | `"Maximum length is <max>"` |
390
+ | `invalidSchemaFormatMsg(field, format)` | `"<field> should match format: <format>"` |
391
+ | `invalidEmailMsg()` | `"Invalid email format"` |
392
+ | `invalidPasswordMsg()` | `"Password should be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character"` |
393
+ | `invalidNumberMsg(field)` | `"Invalid number format for field: <field>"` |
394
+ | `positiveNumberMsg(field)` | `"<field> should be positive"` |
395
+ | `invalidUrlMsg()` | `"Invalid URL"` |
396
+ | `invalidDateFormatMsg()` | `"Invalid date format"` |
397
+ | `futureDateRequiredMsg()` | `"Date must be in the future"` |
401
398
 
402
399
  ```ts
403
400
  import { z } from "zod";
404
401
  import {
405
- requiredFieldMsg,
406
- minLengthMsg,
407
402
  invalidEmailMsg,
403
+ maxLengthMsg,
404
+ minLengthMsg,
408
405
  } from "@mustafahmed1997/learnify-backend";
409
406
 
410
- const schema = z.object({
411
- email: z.string().email(invalidEmailMsg()),
412
- password: z.string().min(8, minLengthMsg(8)),
407
+ const signUpSchema = z.object({
408
+ email: z.email(invalidEmailMsg()),
409
+ name: z.string().min(2, minLengthMsg(2)).max(50, maxLengthMsg(50)),
413
410
  });
414
411
  ```
415
412
 
416
- Available: `requiredFieldMsg(field)`, `minLengthMsg(min)`,
417
- `maxLengthMsg(max)`, `invalidSchemaFormatMsg(field, format)`,
418
- `invalidEmailMsg()`, `invalidPasswordMsg()`, `invalidDateFormatMsg()`,
419
- `futureDateRequiredMsg()`, `invalidNumberMsg(field)`,
420
- `positiveNumberMsg(field)`, `invalidUrlMsg()`.
421
-
422
- ## `ClerkUser` and `req.user` typing
423
-
424
- Importing anything from this package's root automatically loads the Express
425
- `Request` augmentation, so `req.user` is typed as `ClerkUser | undefined`
426
- wherever `express`'s `Request` type is used — no extra `express.d.ts` needed
427
- in the consuming service.
413
+ ### Role utilities and constants
428
414
 
429
415
  ```ts
430
- import type { Request, Response } from "express";
431
- import type { ClerkUser } from "@mustafahmed1997/learnify-backend";
432
-
433
- function exampleController(req: Request, res: Response) {
434
- const clerkId = req.user?.clerkId;
435
- const role = req.user?.userMetadata.role;
416
+ import {
417
+ checkUnique,
418
+ checkValidRoles,
419
+ validRoles,
420
+ type Role,
421
+ } from "@mustafahmed1997/learnify-backend";
436
422
 
437
- res.json({ clerkId, role });
438
- }
423
+ validRoles; // readonly ["STUDENT", "INSTRUCTOR", "ADMIN"]
424
+ checkUnique(["ADMIN", "ADMIN"]); // false (case-sensitive; [] → true)
425
+ checkValidRoles(["ADMIN", "STUDENT"]); // true
426
+ checkValidRoles(["ADMIN", "admin"]); // false (case-sensitive; [] → true)
439
427
  ```
440
428
 
441
- `ClerkUser` shape:
429
+ `checkRole` uses these internally. They're exported for custom role checks,
430
+ for example when validating role updates.
442
431
 
443
- ```ts
444
- type ClerkUser = {
445
- clerkId: string;
446
- userMetadata: {
447
- role: "STUDENT" | "INSTRUCTOR" | "ADMIN";
448
- dbUserId: string;
449
- };
450
- email?: string;
451
- firstName?: string;
452
- lastName?: string;
453
- imageURL?: string;
454
- };
455
- ```
432
+ ### Types
456
433
 
457
- > Note: `checkAuth` attaches `req.user` from `sessionClaims.userMetadata`
458
- > as-is (singular `role`). `checkRole` performs its own authorization check
459
- > directly against `sessionClaims.userMetadata.roles` (plural, an array) —
460
- > it does not read `req.user`. If your Clerk session claims only populate
461
- > `roles`, use `checkRole` for authorization decisions rather than
462
- > `req.user.userMetadata.role`.
434
+ | Type | Definition |
435
+ | --- | --- |
436
+ | `Role` | `"STUDENT" \| "INSTRUCTOR" \| "ADMIN"` |
437
+ | `Location` | `"body" \| "params" \| "query"` |
438
+ | `ValidationErrorItem` | `{ location: Location; field: string; message: string }` |
439
+ | `ClerkUser` | `{ clerkId: string; userMetadata: { role: Role; dbUserId: string }; email?; firstName?; lastName?; imageURL? }` |
440
+ | `JsonResponseOptions<TData, TAdditionalInfo>` | Options accepted by `getJsonResponse` |
441
+ | `RoleCheckMode` | `"All" \| "Exact" \| "Any"` |
442
+ | `CheckRoleOptions` | `{ mode: RoleCheckMode }` |
463
443
 
464
- ## TypeScript usage
444
+ **`req.user` typing.** Importing anything from the package root loads a global
445
+ augmentation that types `req.user` as `ClerkUser | undefined` on Express's
446
+ `Request`. No local `express.d.ts` is needed.
465
447
 
466
- The package ships its own `.d.ts` declarations — no `@types` package needed.
467
- It targets ESM (`"type": "module"`) and expects a `moduleResolution` setting
468
- that supports package `exports` (e.g. `node16`, `nodenext`, or `bundler`).
448
+ > `ClerkUser.userMetadata.role` is a single role taken as-is from the session
449
+ > claims, while `checkRole` authorizes against the `userMetadata.roles` array.
450
+ > Make authorization decisions with `checkRole` rather than `req.user`.
469
451
 
470
- ## Using this package in the Learnify microservices
452
+ ## Error handling
471
453
 
472
- A typical service wires the pieces together in this order:
454
+ There are two paths an error can take:
473
455
 
474
- ```ts
475
- import express from "express";
476
- import { clerkMiddleware } from "@clerk/express";
477
- import {
478
- checkAuth,
479
- checkRole,
480
- validationMiddleware,
481
- globalErrorHandler,
482
- } from "@mustafahmed1997/learnify-backend";
456
+ **1. Responses sent directly by middleware** (`next()` is not called, and
457
+ `globalErrorHandler` doesn't see them):
483
458
 
484
- const app = express();
459
+ | Source | Status | Body |
460
+ | --- | --- | --- |
461
+ | `checkAuth` | `401` | `{ "error": "Unauthenticated. Login first please." }` |
462
+ | `checkRole` | `403` | `{ "message": "User roles not found" }` or `{ "message": "Forbidden" }` |
485
463
 
486
- app.use(express.json());
487
- app.use(clerkMiddleware()); // must run before checkAuth/checkRole
464
+ These bodies don't include a `success` field.
488
465
 
489
- app.post(
490
- "/courses",
491
- checkAuth,
492
- checkRole(["INSTRUCTOR", "ADMIN"]),
493
- validationMiddleware({ body: createCourseSchema }),
494
- createCourseController,
495
- );
466
+ **2. Errors passed to `next()`** and rendered by `globalErrorHandler`. This
467
+ covers `ValidationError` from `validationMiddleware`, errors forwarded by
468
+ `handleCaughtError`, configuration errors from `checkRole`, errors from
469
+ `checkAuth`, and anything thrown or rejected in your handlers (Express 5
470
+ forwards rejected promises automatically).
471
+
472
+ `globalErrorHandler` logs every error with `console.error`, then responds:
473
+
474
+ | Error | Status | Body |
475
+ | --- | --- | --- |
476
+ | `ValidationError` | `400` | `{ "success": false, "error": "Validation failed", "errors": [ { "location": "body", "field": "title", "message": "..." } ] }` |
477
+ | Any other `AppError` | `err.statusCode` | `{ "success": false, "error": err.message }` |
478
+ | Anything else | `500` | `{ "success": false, "error": "Internal server error" }` |
496
479
 
497
- // ...all other routes...
480
+ The `500` body is fixed: messages and stack traces of unexpected errors are
481
+ never sent to clients. Only `AppError` instances get their own status, so a raw
482
+ Prisma error that reaches the handler becomes a `500`. Route `catch` blocks
483
+ through `handleCaughtError` to get `404`/`409`/`400` for Prisma errors:
498
484
 
499
- app.use(globalErrorHandler); // must be registered last
485
+ ```ts
486
+ try {
487
+ await prisma.enrollment.create({ data });
488
+ } catch (error) {
489
+ handleCaughtError(error, next); // P2002 → 409 "Duplicate value for ..."
490
+ }
500
491
  ```
501
492
 
502
- Controllers can then `throw new AppError(...)` / `throw new ValidationError(...)`
503
- (or pass them to `next(...)`) and rely on `globalErrorHandler` to produce a
504
- consistent response shape across every microservice.
493
+ ## Testing
505
494
 
506
- ## Running tests
495
+ Tests use [Vitest](https://vitest.dev) 3 in a Node environment and live in
496
+ `tests/**/*.test.ts` (see `vitest.config.ts`). Clerk is mocked with
497
+ `vi.mock("@clerk/express")`, and Express `req`/`res`/`next` are lightweight
498
+ stubs, so no network, Clerk instance or database is needed.
507
499
 
508
500
  ```bash
509
- npm test # runs the full suite once (vitest run)
510
- npm run test:watch
501
+ npm test # run the whole suite once
502
+ npm run test:watch # watch mode
503
+ npx vitest run tests/checkRole.test.ts # a single file
504
+ npx vitest run -t "handleCaughtError" # tests matching a name
511
505
  ```
512
506
 
513
- ## Type checking
507
+ Coverage isn't configured in this repository. To measure it locally, install
508
+ the V8 provider that matches the Vitest version, then run:
514
509
 
515
510
  ```bash
516
- npm run typecheck
511
+ npm install --no-save @vitest/coverage-v8@3
512
+ npx vitest run --coverage --coverage.include="src/**"
517
513
  ```
518
514
 
519
- ## Building
515
+ ## Build and development
520
516
 
521
- ```bash
522
- npm run build
517
+ | Script | Command | Purpose |
518
+ | --- | --- | --- |
519
+ | `npm run build` | `tsc -p tsconfig.build.json` | Compile `src/` to `dist/` (JS, `.d.ts`, source maps) |
520
+ | `npm run typecheck` | `tsc -p tsconfig.json` | Type-check `src/` and `tests/` without emitting |
521
+ | `npm test` | `vitest run` | Run the test suite once |
522
+ | `npm run test:watch` | `vitest` | Run tests in watch mode |
523
+
524
+ There are no lint or format scripts.
525
+
526
+ ## Project structure
527
+
528
+ ```text
529
+ src/
530
+ index.ts # public entry point: every export lives here
531
+ constants/validRoles.ts # validRoles, Role
532
+ errors/ # AppError, ValidationError, globalErrorHandler, handlePrismaError
533
+ middlewares/ # checkAuth, checkRole, validationMiddleware
534
+ types/ # ClerkUser, Location, Express Request augmentation
535
+ utils/ # response/message helpers, role checks, handleCaughtError
536
+ tests/ # one *.test.ts per module, plus index.test.ts for the export list
537
+ tsconfig.json # strict, node16 modules; includes src and tests (typecheck)
538
+ tsconfig.build.json # extends tsconfig.json; emits src → dist
539
+ vitest.config.ts # Node environment, tests/**/*.test.ts
523
540
  ```
524
541
 
525
- Compiles `src/` to `dist/` (via `tsconfig.build.json`), producing the ESM
526
- output and `.d.ts` declarations referenced by `main`/`types` in
527
- `package.json`.
542
+ ## Contributing
543
+
544
+ 1. **Add the implementation** under the matching `src/` folder. Use explicit
545
+ `.js` extensions in relative imports (required by `node16` module
546
+ resolution).
547
+ 2. **Export it** from `src/index.ts`. Use `export type` for type-only exports.
548
+ 3. **Update the export list** in `tests/index.test.ts`. That test fails if the
549
+ public surface changes unexpectedly.
550
+ 4. **Write tests** in `tests/<name>.test.ts`. Cover success, failure and edge
551
+ cases. Mock `@clerk/express` and stub `req`/`res`/`next` as the existing
552
+ tests do.
553
+ 5. **Verify**: `npm run typecheck && npm test && npm run build`.
554
+ 6. **Document it** in this README: add it to the Features table and the API
555
+ reference.
556
+
557
+ ## Publishing
558
+
559
+ - The package entry is `dist/index.js` with types at `dist/index.d.ts`,
560
+ exposed through a single `"."` export (ESM `import` condition only).
561
+ - Only `dist/` is published (`"files": ["dist"]`). `dist/` is git-ignored.
562
+ - It's published publicly to npm (`publishConfig.access: "public"`).
563
+ - There is no `prepublishOnly` hook, so build before publishing:
564
+
565
+ ```bash
566
+ npm run typecheck && npm test && npm run build
567
+ npm version <patch|minor|major>
568
+ npm publish
569
+ ```
528
570
 
529
- ## What this package does not do
571
+ ## License
530
572
 
531
- - Does not depend on Prisma or any database (`handlePrismaError` only maps
532
- error shapes — it never imports `@prisma/client`).
533
- - Does not call the Auth Service (or any other service) over HTTP.
534
- - Does not manage Clerk session creation — that's handled by `clerkMiddleware()`.
573
+ ISC
@@ -1,7 +1,8 @@
1
1
  import { AppError } from "./AppError.js";
2
2
  import { ValidationError } from "./validationError.js";
3
3
  export function globalErrorHandler(err, req, res, next) {
4
- console.error(err);
4
+ console.error("Inside global error handler : ", err);
5
+ console.log("End of global error logger !!");
5
6
  if (err instanceof ValidationError) {
6
7
  return res.status(err.statusCode).json({
7
8
  success: false,
@@ -1 +1 @@
1
- {"version":3,"file":"globalErrorHandler.js","sourceRoot":"","sources":["../../src/errors/globalErrorHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,kBAAkB,CAChC,GAAY,EACZ,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnB,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,CAAC,OAAO;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,CAAC,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QAC1B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,uBAAuB;KAC/B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;EAoBE"}
1
+ {"version":3,"file":"globalErrorHandler.js","sourceRoot":"","sources":["../../src/errors/globalErrorHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,kBAAkB,CAChC,GAAY,EACZ,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,GAAG,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAE7C,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,CAAC,OAAO;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,CAAC,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QAC1B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,uBAAuB;KAC/B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;EAoBE"}
@@ -22,7 +22,14 @@ export function validationMiddleware(validaitonSchema) {
22
22
  req.params = result.data;
23
23
  break;
24
24
  case "query":
25
- req.query = result.data;
25
+ // Express 5 exposes req.query as a getter-only property, so a
26
+ // plain assignment throws; shadow it with an own data property.
27
+ Object.defineProperty(req, "query", {
28
+ value: result.data,
29
+ writable: true,
30
+ enumerable: true,
31
+ configurable: true,
32
+ });
26
33
  break;
27
34
  }
28
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validationMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/validationMiddleware.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,eAAe,GAEhB,MAAM,8BAA8B,CAAC;AAQtC,MAAM,UAAU,oBAAoB,CAAC,gBAAsC;IACzE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,gBAAgB,GAA0B,EAAE,CAAC;QAEnD,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,MAAM;gBAAE,SAAS;YAEtB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAyB,CAAC,CAAC,CAAC;YAEhE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,gBAAgB,CAAC,IAAI,CACnB,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACrC,QAAQ,EAAE,QAAoB;oBAC9B,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;oBAC3B,OAAO,EAAE,KAAK,CAAC,OAAO;iBACvB,CAAC,CAAC,CACJ,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,QAAQ,QAAoB,EAAE,CAAC;oBAC7B,KAAK,MAAM;wBACT,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;wBACvB,MAAM;oBACR,KAAK,QAAQ;wBACX,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,IAAyB,CAAC;wBAC9C,MAAM;oBACR,KAAK,OAAO;wBACV,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,IAAwB,CAAC;wBAC5C,MAAM;gBACV,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,IAAI,eAAe,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"validationMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/validationMiddleware.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,eAAe,GAEhB,MAAM,8BAA8B,CAAC;AAQtC,MAAM,UAAU,oBAAoB,CAAC,gBAAsC;IACzE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,gBAAgB,GAA0B,EAAE,CAAC;QAEnD,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,MAAM;gBAAE,SAAS;YAEtB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAyB,CAAC,CAAC,CAAC;YAEhE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,gBAAgB,CAAC,IAAI,CACnB,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACrC,QAAQ,EAAE,QAAoB;oBAC9B,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;oBAC3B,OAAO,EAAE,KAAK,CAAC,OAAO;iBACvB,CAAC,CAAC,CACJ,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,QAAQ,QAAoB,EAAE,CAAC;oBAC7B,KAAK,MAAM;wBACT,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;wBACvB,MAAM;oBACR,KAAK,QAAQ;wBACX,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,IAAyB,CAAC;wBAC9C,MAAM;oBACR,KAAK,OAAO;wBACV,8DAA8D;wBAC9D,gEAAgE;wBAChE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE;4BAClC,KAAK,EAAE,MAAM,CAAC,IAAI;4BAClB,QAAQ,EAAE,IAAI;4BACd,UAAU,EAAE,IAAI;4BAChB,YAAY,EAAE,IAAI;yBACnB,CAAC,CAAC;wBACH,MAAM;gBACV,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,IAAI,eAAe,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -2,7 +2,7 @@ import { validRoles } from "../constants/validRoles.js";
2
2
  export type ClerkUser = {
3
3
  clerkId: string;
4
4
  userMetadata: {
5
- role: (typeof validRoles)[number];
5
+ roles: (typeof validRoles)[number][];
6
6
  dbUserId: string;
7
7
  };
8
8
  email?: string;
@@ -4,7 +4,13 @@ export function handleCaughtError(error, next) {
4
4
  if (error instanceof AppError) {
5
5
  return next(error);
6
6
  }
7
- const prismaError = handlePrismaError(error);
8
- return next(prismaError);
7
+ const { status, message } = handlePrismaError(error);
8
+ // Client errors (e.g. P2002, P2025) become an AppError so globalErrorHandler
9
+ // responds with the mapped status. Server errors are forwarded untouched so
10
+ // their internal messages are logged but never sent to the client.
11
+ if (status < 500) {
12
+ return next(new AppError(message, status));
13
+ }
14
+ return next(error);
9
15
  }
10
16
  //# sourceMappingURL=handleCaughtError.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handleCaughtError.js","sourceRoot":"","sources":["../../src/utils/handleCaughtError.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,IAAkB;IAClE,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,WAAW,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC;AAC3B,CAAC"}
1
+ {"version":3,"file":"handleCaughtError.js","sourceRoot":"","sources":["../../src/utils/handleCaughtError.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,IAAkB;IAClE,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAErD,6EAA6E;IAC7E,4EAA4E;IAC5E,mEAAmE;IACnE,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mustafahmed1997/learnify-backend",
3
- "version": "1.9.0",
3
+ "version": "2.0.1",
4
4
  "description": "Shared Clerk-based auth middlewares, role checking, error utilities, and Express types for Learnify backend microservices.",
5
5
  "keywords": [
6
6
  "express",