@mohasinac/next 1.0.0 → 1.1.0

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/dist/index.cjs CHANGED
@@ -35,12 +35,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
35
35
  var index_exports = {};
36
36
  __export(index_exports, {
37
37
  createApiErrorHandler: () => createApiErrorHandler,
38
- createRouteHandler: () => createRouteHandler
38
+ createApiHandlerFactory: () => createApiHandlerFactory,
39
+ createRouteHandler: () => createRouteHandler,
40
+ getBooleanParam: () => getBooleanParam,
41
+ getNumberParam: () => getNumberParam,
42
+ getOptionalSessionCookie: () => getOptionalSessionCookie,
43
+ getRequiredSessionCookie: () => getRequiredSessionCookie,
44
+ getSearchParams: () => getSearchParams,
45
+ getStringParam: () => getStringParam,
46
+ invalidateCache: () => invalidateCache,
47
+ withCache: () => withCache
39
48
  });
40
49
  module.exports = __toCommonJS(index_exports);
41
50
 
42
51
  // src/api/errorHandler.ts
43
- var import_server = require("next/server");
52
+ var import_server = require("next/server.js");
44
53
  function createApiErrorHandler(options) {
45
54
  const {
46
55
  isAppError,
@@ -86,7 +95,7 @@ function createApiErrorHandler(options) {
86
95
  }
87
96
 
88
97
  // src/api/routeHandler.ts
89
- var import_server2 = require("next/server");
98
+ var import_server2 = require("next/server.js");
90
99
  var import_contracts = require("@mohasinac/contracts");
91
100
  function readSessionCookie(request) {
92
101
  var _a;
@@ -171,15 +180,222 @@ function createRouteHandler(options) {
171
180
  const status = typeof (err == null ? void 0 : err.status) === "number" ? err.status : 500;
172
181
  const message = err instanceof Error ? err.message : "Internal server error";
173
182
  console.error(`[createRouteHandler] ${request.method} failed`, err);
174
- return import_server2.NextResponse.json(
175
- { success: false, error: message },
176
- { status }
177
- );
183
+ return import_server2.NextResponse.json({ success: false, error: message }, { status });
184
+ }
185
+ };
186
+ }
187
+
188
+ // src/api/apiHandler.ts
189
+ function createApiHandlerFactory(deps) {
190
+ return function createApiHandler(options) {
191
+ return async (request, context) => {
192
+ const startMs = performance.now();
193
+ try {
194
+ let rateLimitHeaders;
195
+ if (options.rateLimit) {
196
+ const rateLimitResult = await deps.applyRateLimit(
197
+ request,
198
+ options.rateLimit
199
+ );
200
+ rateLimitHeaders = {
201
+ "RateLimit-Limit": String(rateLimitResult.limit),
202
+ "RateLimit-Remaining": String(rateLimitResult.remaining),
203
+ "RateLimit-Reset": String(rateLimitResult.reset)
204
+ };
205
+ if (!rateLimitResult.success) {
206
+ return new Response(
207
+ JSON.stringify({
208
+ success: false,
209
+ error: deps.getRateLimitExceededMessage()
210
+ }),
211
+ {
212
+ status: 429,
213
+ headers: __spreadValues({
214
+ "Content-Type": "application/json"
215
+ }, rateLimitHeaders)
216
+ }
217
+ );
218
+ }
219
+ }
220
+ let user;
221
+ if (options.roles && options.roles.length > 0) {
222
+ user = await deps.requireRoleFromRequest(request, options.roles);
223
+ } else if (options.auth) {
224
+ user = await deps.requireAuthFromRequest(request);
225
+ }
226
+ let validatedBody;
227
+ if (options.schema) {
228
+ if (typeof options.schema.safeParse === "function") {
229
+ const body = await request.json();
230
+ const result = options.schema.safeParse(body);
231
+ if (!result.success) {
232
+ return deps.errorResponse(
233
+ "Validation failed",
234
+ 400,
235
+ result.error.issues
236
+ );
237
+ }
238
+ validatedBody = result.data;
239
+ } else {
240
+ try {
241
+ validatedBody = await request.json();
242
+ } catch (e) {
243
+ validatedBody = void 0;
244
+ }
245
+ }
246
+ }
247
+ const resolvedParams = (context == null ? void 0 : context.params) ? await context.params : void 0;
248
+ const response = await options.handler({
249
+ request,
250
+ user,
251
+ body: validatedBody,
252
+ params: resolvedParams
253
+ });
254
+ response.headers.set("Access-Control-Max-Age", "86400");
255
+ if (rateLimitHeaders) {
256
+ for (const [key, value] of Object.entries(rateLimitHeaders)) {
257
+ response.headers.set(key, value);
258
+ }
259
+ }
260
+ deps.logTiming({
261
+ method: request.method,
262
+ path: new URL(request.url).pathname,
263
+ status: response.status,
264
+ durationMs: Math.round(performance.now() - startMs)
265
+ });
266
+ return response;
267
+ } catch (error) {
268
+ deps.logTiming({
269
+ method: request.method,
270
+ path: new URL(request.url).pathname,
271
+ durationMs: Math.round(performance.now() - startMs),
272
+ error: error instanceof Error ? error.message : String(error)
273
+ });
274
+ return deps.handleApiError(error);
275
+ }
276
+ };
277
+ };
278
+ }
279
+
280
+ // src/request-helpers.ts
281
+ var import_errors = require("@mohasinac/errors");
282
+ function readCookieFromHeader(request, name) {
283
+ var _a;
284
+ const cookieHeader = (_a = request.headers.get("cookie")) != null ? _a : "";
285
+ const match = cookieHeader.match(
286
+ new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
287
+ );
288
+ return match ? decodeURIComponent(match[1]) : void 0;
289
+ }
290
+ function getSearchParams(request) {
291
+ return new URL(request.url).searchParams;
292
+ }
293
+ function getOptionalSessionCookie(request) {
294
+ var _a;
295
+ const cookieValue = (_a = request.cookies) == null ? void 0 : _a.get("__session");
296
+ if (typeof cookieValue === "string") return cookieValue;
297
+ if (cookieValue && typeof cookieValue === "object") return cookieValue.value;
298
+ return readCookieFromHeader(request, "__session");
299
+ }
300
+ function getRequiredSessionCookie(request) {
301
+ const cookie = getOptionalSessionCookie(request);
302
+ if (!cookie) throw new import_errors.AuthenticationError("Unauthorized");
303
+ return cookie;
304
+ }
305
+ function getBooleanParam(searchParams, key) {
306
+ const value = searchParams.get(key);
307
+ if (value === null) return void 0;
308
+ return value === "true";
309
+ }
310
+ function getStringParam(searchParams, key) {
311
+ const value = searchParams.get(key);
312
+ if (!value) return void 0;
313
+ return value;
314
+ }
315
+ function getNumberParam(searchParams, key, fallback, options) {
316
+ const rawValue = searchParams.get(key);
317
+ const parsed = rawValue ? Number.parseInt(rawValue, 10) : fallback;
318
+ const safeValue = Number.isNaN(parsed) ? fallback : parsed;
319
+ if (typeof (options == null ? void 0 : options.min) === "number" && safeValue < options.min)
320
+ return options.min;
321
+ if (typeof (options == null ? void 0 : options.max) === "number" && safeValue > options.max)
322
+ return options.max;
323
+ return safeValue;
324
+ }
325
+
326
+ // src/cache-middleware.ts
327
+ var import_server3 = require("next/server.js");
328
+ var import_core = require("@mohasinac/core");
329
+ var DEFAULT_TTL = 5 * 60 * 1e3;
330
+ var cache = import_core.CacheManager.getInstance(500);
331
+ function buildCacheKey(request, config) {
332
+ if (config.keyGenerator) return config.keyGenerator(request);
333
+ const url = new URL(request.url);
334
+ if (config.includeQuery !== false && url.search)
335
+ return `${url.pathname}${url.search}`;
336
+ return url.pathname;
337
+ }
338
+ function isCacheable(request, config) {
339
+ var _a;
340
+ if (config.bypassCache) return false;
341
+ const methods = (_a = config.methods) != null ? _a : ["GET"];
342
+ return methods.includes(request.method);
343
+ }
344
+ function withCache(handler, config = {}) {
345
+ return async (request, ...args) => {
346
+ var _a;
347
+ if (!isCacheable(request, config)) {
348
+ return handler(request, ...args);
349
+ }
350
+ const key = buildCacheKey(request, config);
351
+ const cached = cache.get(key);
352
+ if (cached) {
353
+ return new import_server3.NextResponse(cached.body, {
354
+ status: cached.status,
355
+ headers: cached.headers
356
+ });
178
357
  }
358
+ const response = await handler(request, ...args);
359
+ const body = await response.text();
360
+ const headers = {};
361
+ response.headers.forEach((value, key2) => {
362
+ headers[key2] = value;
363
+ });
364
+ cache.set(
365
+ key,
366
+ { body, status: response.status, headers },
367
+ { ttl: (_a = config.ttl) != null ? _a : DEFAULT_TTL }
368
+ );
369
+ return new import_server3.NextResponse(body, { status: response.status, headers });
179
370
  };
180
371
  }
372
+ function invalidateCache(pattern) {
373
+ if (!pattern) {
374
+ cache.clear();
375
+ return;
376
+ }
377
+ const keys = cache.keys();
378
+ if (typeof pattern === "string") {
379
+ for (const key of keys) {
380
+ if (key.startsWith(pattern)) cache.delete(key);
381
+ }
382
+ } else {
383
+ for (const key of keys) {
384
+ if (pattern.test(key)) cache.delete(key);
385
+ }
386
+ }
387
+ }
181
388
  // Annotate the CommonJS export names for ESM import in node:
182
389
  0 && (module.exports = {
183
390
  createApiErrorHandler,
184
- createRouteHandler
391
+ createApiHandlerFactory,
392
+ createRouteHandler,
393
+ getBooleanParam,
394
+ getNumberParam,
395
+ getOptionalSessionCookie,
396
+ getRequiredSessionCookie,
397
+ getSearchParams,
398
+ getStringParam,
399
+ invalidateCache,
400
+ withCache
185
401
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextResponse, NextRequest } from 'next/server.js';
2
2
 
3
3
  /**
4
4
  * IAuthVerifier — Injectable auth interface for createApiHandler.
@@ -172,4 +172,111 @@ declare function createRouteHandler<TInput = unknown, TParams = Record<string, s
172
172
  params: Promise<TParams>;
173
173
  }) => Promise<NextResponse>;
174
174
 
175
- export { type ApiErrorHandlerOptions, type AuthVerifiedUser, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createRouteHandler };
175
+ interface SafeParseSchema<TInput> {
176
+ safeParse: (input: unknown) => {
177
+ success: true;
178
+ data: TInput;
179
+ } | {
180
+ success: false;
181
+ error: {
182
+ issues?: unknown[];
183
+ };
184
+ };
185
+ }
186
+ interface ApiRateLimitResult {
187
+ success: boolean;
188
+ limit: number;
189
+ remaining: number;
190
+ reset: number;
191
+ }
192
+ interface ApiHandlerOptions<TInput, TParams extends Record<string, string>, TRole, TRateLimitConfig, TUser> {
193
+ auth?: boolean;
194
+ roles?: TRole[];
195
+ rateLimit?: TRateLimitConfig;
196
+ schema?: SafeParseSchema<TInput>;
197
+ handler: (ctx: {
198
+ request: Request;
199
+ user?: TUser;
200
+ body?: TInput;
201
+ params?: TParams;
202
+ }) => Promise<Response>;
203
+ }
204
+ interface ApiHandlerFactoryDeps<TRole, TRateLimitConfig, TUser> {
205
+ applyRateLimit: (request: Request, config: TRateLimitConfig) => Promise<ApiRateLimitResult>;
206
+ requireAuthFromRequest: (request: Request) => Promise<TUser>;
207
+ requireRoleFromRequest: (request: Request, roles: TRole[]) => Promise<TUser>;
208
+ errorResponse: (message: string, status: number, issues?: unknown) => Response;
209
+ handleApiError: (error: unknown) => Response;
210
+ getRateLimitExceededMessage: () => string;
211
+ logTiming: (entry: {
212
+ method: string;
213
+ path: string;
214
+ status?: number;
215
+ durationMs: number;
216
+ error?: string;
217
+ }) => void;
218
+ }
219
+ declare function createApiHandlerFactory<TRole, TRateLimitConfig, TUser>(deps: ApiHandlerFactoryDeps<TRole, TRateLimitConfig, TUser>): <TInput = unknown, TParams extends Record<string, string> = Record<string, string>>(options: ApiHandlerOptions<TInput, TParams, TRole, TRateLimitConfig, TUser>) => (request: Request, context: {
220
+ params: Promise<TParams>;
221
+ }) => Promise<Response>;
222
+
223
+ /**
224
+ * API Request Helpers
225
+ *
226
+ * Generic utilities for API route request parsing and session extraction.
227
+ * Uses structural request types (not NextRequest) to avoid cross-package
228
+ * type identity issues when multiple Next.js installs exist in a monorepo.
229
+ */
230
+ interface CookieStoreLike {
231
+ get(name: string): {
232
+ value?: string;
233
+ } | string | undefined;
234
+ }
235
+ type CookieCapableRequest = Request & {
236
+ cookies?: CookieStoreLike;
237
+ };
238
+ declare function getSearchParams(request: Request): URLSearchParams;
239
+ declare function getOptionalSessionCookie(request: CookieCapableRequest): string | undefined;
240
+ declare function getRequiredSessionCookie(request: CookieCapableRequest): string;
241
+ declare function getBooleanParam(searchParams: URLSearchParams, key: string): boolean | undefined;
242
+ declare function getStringParam(searchParams: URLSearchParams, key: string): string | undefined;
243
+ declare function getNumberParam(searchParams: URLSearchParams, key: string, fallback: number, options?: {
244
+ min?: number;
245
+ max?: number;
246
+ }): number;
247
+
248
+ /**
249
+ * API Response Cache Middleware
250
+ *
251
+ * Wraps a Next.js API handler with in-memory response caching via CacheManager.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * export const GET = withCache(
256
+ * async (request) => { ... return NextResponse.json(data); },
257
+ * { ttl: 5 * 60 * 1000 }
258
+ * );
259
+ * ```
260
+ */
261
+
262
+ interface CacheConfig {
263
+ /** Time to live in milliseconds. Default: 5 minutes. */
264
+ ttl?: number;
265
+ /** Include query string in cache key. Default: true. */
266
+ includeQuery?: boolean;
267
+ /** Custom cache key generator. */
268
+ keyGenerator?: (request: NextRequest) => string;
269
+ /** Bypass caching entirely. Default: false. */
270
+ bypassCache?: boolean;
271
+ /** Only cache for these HTTP methods. Default: ['GET']. */
272
+ methods?: string[];
273
+ }
274
+ type Handler = (request: NextRequest, ...args: unknown[]) => Promise<NextResponse>;
275
+ declare function withCache(handler: Handler, config?: CacheConfig): Handler;
276
+ /**
277
+ * Invalidate cache entries by key prefix or regex pattern.
278
+ * Pass no argument to clear the entire cache.
279
+ */
280
+ declare function invalidateCache(pattern?: string | RegExp): void;
281
+
282
+ export { type ApiErrorHandlerOptions, type ApiHandlerFactoryDeps, type ApiHandlerOptions, type ApiRateLimitResult, type AuthVerifiedUser, type CacheConfig, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createApiHandlerFactory, createRouteHandler, getBooleanParam, getNumberParam, getOptionalSessionCookie, getRequiredSessionCookie, getSearchParams, getStringParam, invalidateCache, withCache };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextResponse } from 'next/server';
1
+ import { NextResponse, NextRequest } from 'next/server.js';
2
2
 
3
3
  /**
4
4
  * IAuthVerifier — Injectable auth interface for createApiHandler.
@@ -172,4 +172,111 @@ declare function createRouteHandler<TInput = unknown, TParams = Record<string, s
172
172
  params: Promise<TParams>;
173
173
  }) => Promise<NextResponse>;
174
174
 
175
- export { type ApiErrorHandlerOptions, type AuthVerifiedUser, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createRouteHandler };
175
+ interface SafeParseSchema<TInput> {
176
+ safeParse: (input: unknown) => {
177
+ success: true;
178
+ data: TInput;
179
+ } | {
180
+ success: false;
181
+ error: {
182
+ issues?: unknown[];
183
+ };
184
+ };
185
+ }
186
+ interface ApiRateLimitResult {
187
+ success: boolean;
188
+ limit: number;
189
+ remaining: number;
190
+ reset: number;
191
+ }
192
+ interface ApiHandlerOptions<TInput, TParams extends Record<string, string>, TRole, TRateLimitConfig, TUser> {
193
+ auth?: boolean;
194
+ roles?: TRole[];
195
+ rateLimit?: TRateLimitConfig;
196
+ schema?: SafeParseSchema<TInput>;
197
+ handler: (ctx: {
198
+ request: Request;
199
+ user?: TUser;
200
+ body?: TInput;
201
+ params?: TParams;
202
+ }) => Promise<Response>;
203
+ }
204
+ interface ApiHandlerFactoryDeps<TRole, TRateLimitConfig, TUser> {
205
+ applyRateLimit: (request: Request, config: TRateLimitConfig) => Promise<ApiRateLimitResult>;
206
+ requireAuthFromRequest: (request: Request) => Promise<TUser>;
207
+ requireRoleFromRequest: (request: Request, roles: TRole[]) => Promise<TUser>;
208
+ errorResponse: (message: string, status: number, issues?: unknown) => Response;
209
+ handleApiError: (error: unknown) => Response;
210
+ getRateLimitExceededMessage: () => string;
211
+ logTiming: (entry: {
212
+ method: string;
213
+ path: string;
214
+ status?: number;
215
+ durationMs: number;
216
+ error?: string;
217
+ }) => void;
218
+ }
219
+ declare function createApiHandlerFactory<TRole, TRateLimitConfig, TUser>(deps: ApiHandlerFactoryDeps<TRole, TRateLimitConfig, TUser>): <TInput = unknown, TParams extends Record<string, string> = Record<string, string>>(options: ApiHandlerOptions<TInput, TParams, TRole, TRateLimitConfig, TUser>) => (request: Request, context: {
220
+ params: Promise<TParams>;
221
+ }) => Promise<Response>;
222
+
223
+ /**
224
+ * API Request Helpers
225
+ *
226
+ * Generic utilities for API route request parsing and session extraction.
227
+ * Uses structural request types (not NextRequest) to avoid cross-package
228
+ * type identity issues when multiple Next.js installs exist in a monorepo.
229
+ */
230
+ interface CookieStoreLike {
231
+ get(name: string): {
232
+ value?: string;
233
+ } | string | undefined;
234
+ }
235
+ type CookieCapableRequest = Request & {
236
+ cookies?: CookieStoreLike;
237
+ };
238
+ declare function getSearchParams(request: Request): URLSearchParams;
239
+ declare function getOptionalSessionCookie(request: CookieCapableRequest): string | undefined;
240
+ declare function getRequiredSessionCookie(request: CookieCapableRequest): string;
241
+ declare function getBooleanParam(searchParams: URLSearchParams, key: string): boolean | undefined;
242
+ declare function getStringParam(searchParams: URLSearchParams, key: string): string | undefined;
243
+ declare function getNumberParam(searchParams: URLSearchParams, key: string, fallback: number, options?: {
244
+ min?: number;
245
+ max?: number;
246
+ }): number;
247
+
248
+ /**
249
+ * API Response Cache Middleware
250
+ *
251
+ * Wraps a Next.js API handler with in-memory response caching via CacheManager.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * export const GET = withCache(
256
+ * async (request) => { ... return NextResponse.json(data); },
257
+ * { ttl: 5 * 60 * 1000 }
258
+ * );
259
+ * ```
260
+ */
261
+
262
+ interface CacheConfig {
263
+ /** Time to live in milliseconds. Default: 5 minutes. */
264
+ ttl?: number;
265
+ /** Include query string in cache key. Default: true. */
266
+ includeQuery?: boolean;
267
+ /** Custom cache key generator. */
268
+ keyGenerator?: (request: NextRequest) => string;
269
+ /** Bypass caching entirely. Default: false. */
270
+ bypassCache?: boolean;
271
+ /** Only cache for these HTTP methods. Default: ['GET']. */
272
+ methods?: string[];
273
+ }
274
+ type Handler = (request: NextRequest, ...args: unknown[]) => Promise<NextResponse>;
275
+ declare function withCache(handler: Handler, config?: CacheConfig): Handler;
276
+ /**
277
+ * Invalidate cache entries by key prefix or regex pattern.
278
+ * Pass no argument to clear the entire cache.
279
+ */
280
+ declare function invalidateCache(pattern?: string | RegExp): void;
281
+
282
+ export { type ApiErrorHandlerOptions, type ApiHandlerFactoryDeps, type ApiHandlerOptions, type ApiRateLimitResult, type AuthVerifiedUser, type CacheConfig, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createApiHandlerFactory, createRouteHandler, getBooleanParam, getNumberParam, getOptionalSessionCookie, getRequiredSessionCookie, getSearchParams, getStringParam, invalidateCache, withCache };
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ var __spreadValues = (a, b) => {
16
16
  };
17
17
 
18
18
  // src/api/errorHandler.ts
19
- import { NextResponse } from "next/server";
19
+ import { NextResponse } from "next/server.js";
20
20
  function createApiErrorHandler(options) {
21
21
  const {
22
22
  isAppError,
@@ -62,7 +62,7 @@ function createApiErrorHandler(options) {
62
62
  }
63
63
 
64
64
  // src/api/routeHandler.ts
65
- import { NextResponse as NextResponse2 } from "next/server";
65
+ import { NextResponse as NextResponse2 } from "next/server.js";
66
66
  import { getProviders } from "@mohasinac/contracts";
67
67
  function readSessionCookie(request) {
68
68
  var _a;
@@ -147,14 +147,221 @@ function createRouteHandler(options) {
147
147
  const status = typeof (err == null ? void 0 : err.status) === "number" ? err.status : 500;
148
148
  const message = err instanceof Error ? err.message : "Internal server error";
149
149
  console.error(`[createRouteHandler] ${request.method} failed`, err);
150
- return NextResponse2.json(
151
- { success: false, error: message },
152
- { status }
153
- );
150
+ return NextResponse2.json({ success: false, error: message }, { status });
151
+ }
152
+ };
153
+ }
154
+
155
+ // src/api/apiHandler.ts
156
+ function createApiHandlerFactory(deps) {
157
+ return function createApiHandler(options) {
158
+ return async (request, context) => {
159
+ const startMs = performance.now();
160
+ try {
161
+ let rateLimitHeaders;
162
+ if (options.rateLimit) {
163
+ const rateLimitResult = await deps.applyRateLimit(
164
+ request,
165
+ options.rateLimit
166
+ );
167
+ rateLimitHeaders = {
168
+ "RateLimit-Limit": String(rateLimitResult.limit),
169
+ "RateLimit-Remaining": String(rateLimitResult.remaining),
170
+ "RateLimit-Reset": String(rateLimitResult.reset)
171
+ };
172
+ if (!rateLimitResult.success) {
173
+ return new Response(
174
+ JSON.stringify({
175
+ success: false,
176
+ error: deps.getRateLimitExceededMessage()
177
+ }),
178
+ {
179
+ status: 429,
180
+ headers: __spreadValues({
181
+ "Content-Type": "application/json"
182
+ }, rateLimitHeaders)
183
+ }
184
+ );
185
+ }
186
+ }
187
+ let user;
188
+ if (options.roles && options.roles.length > 0) {
189
+ user = await deps.requireRoleFromRequest(request, options.roles);
190
+ } else if (options.auth) {
191
+ user = await deps.requireAuthFromRequest(request);
192
+ }
193
+ let validatedBody;
194
+ if (options.schema) {
195
+ if (typeof options.schema.safeParse === "function") {
196
+ const body = await request.json();
197
+ const result = options.schema.safeParse(body);
198
+ if (!result.success) {
199
+ return deps.errorResponse(
200
+ "Validation failed",
201
+ 400,
202
+ result.error.issues
203
+ );
204
+ }
205
+ validatedBody = result.data;
206
+ } else {
207
+ try {
208
+ validatedBody = await request.json();
209
+ } catch (e) {
210
+ validatedBody = void 0;
211
+ }
212
+ }
213
+ }
214
+ const resolvedParams = (context == null ? void 0 : context.params) ? await context.params : void 0;
215
+ const response = await options.handler({
216
+ request,
217
+ user,
218
+ body: validatedBody,
219
+ params: resolvedParams
220
+ });
221
+ response.headers.set("Access-Control-Max-Age", "86400");
222
+ if (rateLimitHeaders) {
223
+ for (const [key, value] of Object.entries(rateLimitHeaders)) {
224
+ response.headers.set(key, value);
225
+ }
226
+ }
227
+ deps.logTiming({
228
+ method: request.method,
229
+ path: new URL(request.url).pathname,
230
+ status: response.status,
231
+ durationMs: Math.round(performance.now() - startMs)
232
+ });
233
+ return response;
234
+ } catch (error) {
235
+ deps.logTiming({
236
+ method: request.method,
237
+ path: new URL(request.url).pathname,
238
+ durationMs: Math.round(performance.now() - startMs),
239
+ error: error instanceof Error ? error.message : String(error)
240
+ });
241
+ return deps.handleApiError(error);
242
+ }
243
+ };
244
+ };
245
+ }
246
+
247
+ // src/request-helpers.ts
248
+ import { AuthenticationError } from "@mohasinac/errors";
249
+ function readCookieFromHeader(request, name) {
250
+ var _a;
251
+ const cookieHeader = (_a = request.headers.get("cookie")) != null ? _a : "";
252
+ const match = cookieHeader.match(
253
+ new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
254
+ );
255
+ return match ? decodeURIComponent(match[1]) : void 0;
256
+ }
257
+ function getSearchParams(request) {
258
+ return new URL(request.url).searchParams;
259
+ }
260
+ function getOptionalSessionCookie(request) {
261
+ var _a;
262
+ const cookieValue = (_a = request.cookies) == null ? void 0 : _a.get("__session");
263
+ if (typeof cookieValue === "string") return cookieValue;
264
+ if (cookieValue && typeof cookieValue === "object") return cookieValue.value;
265
+ return readCookieFromHeader(request, "__session");
266
+ }
267
+ function getRequiredSessionCookie(request) {
268
+ const cookie = getOptionalSessionCookie(request);
269
+ if (!cookie) throw new AuthenticationError("Unauthorized");
270
+ return cookie;
271
+ }
272
+ function getBooleanParam(searchParams, key) {
273
+ const value = searchParams.get(key);
274
+ if (value === null) return void 0;
275
+ return value === "true";
276
+ }
277
+ function getStringParam(searchParams, key) {
278
+ const value = searchParams.get(key);
279
+ if (!value) return void 0;
280
+ return value;
281
+ }
282
+ function getNumberParam(searchParams, key, fallback, options) {
283
+ const rawValue = searchParams.get(key);
284
+ const parsed = rawValue ? Number.parseInt(rawValue, 10) : fallback;
285
+ const safeValue = Number.isNaN(parsed) ? fallback : parsed;
286
+ if (typeof (options == null ? void 0 : options.min) === "number" && safeValue < options.min)
287
+ return options.min;
288
+ if (typeof (options == null ? void 0 : options.max) === "number" && safeValue > options.max)
289
+ return options.max;
290
+ return safeValue;
291
+ }
292
+
293
+ // src/cache-middleware.ts
294
+ import { NextResponse as NextResponse3 } from "next/server.js";
295
+ import { CacheManager } from "@mohasinac/core";
296
+ var DEFAULT_TTL = 5 * 60 * 1e3;
297
+ var cache = CacheManager.getInstance(500);
298
+ function buildCacheKey(request, config) {
299
+ if (config.keyGenerator) return config.keyGenerator(request);
300
+ const url = new URL(request.url);
301
+ if (config.includeQuery !== false && url.search)
302
+ return `${url.pathname}${url.search}`;
303
+ return url.pathname;
304
+ }
305
+ function isCacheable(request, config) {
306
+ var _a;
307
+ if (config.bypassCache) return false;
308
+ const methods = (_a = config.methods) != null ? _a : ["GET"];
309
+ return methods.includes(request.method);
310
+ }
311
+ function withCache(handler, config = {}) {
312
+ return async (request, ...args) => {
313
+ var _a;
314
+ if (!isCacheable(request, config)) {
315
+ return handler(request, ...args);
316
+ }
317
+ const key = buildCacheKey(request, config);
318
+ const cached = cache.get(key);
319
+ if (cached) {
320
+ return new NextResponse3(cached.body, {
321
+ status: cached.status,
322
+ headers: cached.headers
323
+ });
154
324
  }
325
+ const response = await handler(request, ...args);
326
+ const body = await response.text();
327
+ const headers = {};
328
+ response.headers.forEach((value, key2) => {
329
+ headers[key2] = value;
330
+ });
331
+ cache.set(
332
+ key,
333
+ { body, status: response.status, headers },
334
+ { ttl: (_a = config.ttl) != null ? _a : DEFAULT_TTL }
335
+ );
336
+ return new NextResponse3(body, { status: response.status, headers });
155
337
  };
156
338
  }
339
+ function invalidateCache(pattern) {
340
+ if (!pattern) {
341
+ cache.clear();
342
+ return;
343
+ }
344
+ const keys = cache.keys();
345
+ if (typeof pattern === "string") {
346
+ for (const key of keys) {
347
+ if (key.startsWith(pattern)) cache.delete(key);
348
+ }
349
+ } else {
350
+ for (const key of keys) {
351
+ if (pattern.test(key)) cache.delete(key);
352
+ }
353
+ }
354
+ }
157
355
  export {
158
356
  createApiErrorHandler,
159
- createRouteHandler
357
+ createApiHandlerFactory,
358
+ createRouteHandler,
359
+ getBooleanParam,
360
+ getNumberParam,
361
+ getOptionalSessionCookie,
362
+ getRequiredSessionCookie,
363
+ getSearchParams,
364
+ getStringParam,
365
+ invalidateCache,
366
+ withCache
160
367
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/next",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,7 +29,9 @@
29
29
  "zod": ">=3"
30
30
  },
31
31
  "dependencies": {
32
- "@mohasinac/contracts": "^1.0.0"
32
+ "@mohasinac/contracts": "^1.1.0",
33
+ "@mohasinac/core": "^1.1.0",
34
+ "@mohasinac/errors": "^1.1.0"
33
35
  },
34
36
  "devDependencies": {
35
37
  "tsup": "^8.5.0",