@sugardarius/anzen 1.1.3 → 2.0.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
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }function E(e){return e!==null&&(typeof e=="object"||typeof e=="function")&&typeof e.then=="function"}function w(e,a){if(E(e))throw new Error(a)}function g(e=!1){let a=e||process.env.NODE_ENV!=="production";return{info:(t,...n)=>{a&&console.log(t,...n)},error:(t,...n)=>{a&&console.error(t,...n)},warn:(t,...n)=>{a&&console.warn(t,...n)}}}function D(){let e=null,a=null;return{start:()=>{e=performance.now()},stop:()=>{if(e===null)throw new Error("Execution clock was not started.");a=performance.now()},get:()=>{if(!e||!a)throw new Error("Execution clock has not been started or stopped.");return`${(a-e).toFixed(2)}ms`}}}function I(e,a,t="Validation must be synchronous but schema returned a Promise."){let n=e["~standard"].validate(a);return w(n,t),n}var A=(e,a)=>a in e;function h(e,a){let t={},n=[];for(let u in e){if(!A(e,u))continue;let i=e[u]["~standard"].validate(a[u]);if(w(i,`Validation must be synchronous, but ${u} returned a Promise.`),i.issues){n.push(...i.issues.map(p=>({...p,path:[u,..._nullishCoalesce(p.path, () => ([]))]})));continue}t[u]=i.value}return n.length>0?{issues:n}:{value:t}}var C="[unknown:route:handler]",F=async e=>{if(_optionalChain([e, 'access', _ => _.headers, 'access', _2 => _2.get, 'call', _3 => _3("content-type"), 'optionalAccess', _4 => _4.startsWith, 'call', _5 => _5("application/json")]))return await e.json();let t=await e.text();return JSON.parse(t)};function H(e,a){if(e.body&&e.formData)throw new Error("You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.");let t=g(e.debug),n=_nullishCoalesce(e.id, () => (C)),u=_nullishCoalesce(e.onErrorResponse, () => ((r=>(t.error(`\u{1F6D1} Unexpected error in route handler '${n}'`,r),new Response("Internal server error",{status:500}))))),i=_nullishCoalesce(e.onSegmentsValidationErrorResponse, () => ((r=>(t.error(`\u{1F6D1} Invalid segments for route handler '${n}':`,r),new Response("Invalid segments",{status:400}))))),p=_nullishCoalesce(e.onSearchParamsValidationErrorResponse, () => ((r=>(t.error(`\u{1F6D1} Invalid search params for route handler '${n}':`,r),new Response("Invalid search params",{status:400}))))),P=_nullishCoalesce(e.onBodyValidationErrorResponse, () => ((r=>(t.error(`\u{1F6D1} Invalid body for route handler '${n}':`,r),new Response("Invalid body",{status:400}))))),v=_nullishCoalesce(e.onFormDataValidationErrorResponse, () => ((r=>(t.error(`\u{1F6D1} Invalid form data for route handler '${n}':`,r),new Response("Invalid form data",{status:400}))))),b=_nullishCoalesce(e.authorize, () => ((async()=>{})));return async function(r,O){let c=D();c.start(),t.info(`\u{1F504} Running route handler '${n}'`),t.info(`\u{1F449}\u{1F3FB} Request ${r.method} ${r.url}`);let m=new URL(r.url),k=r.clone(),f=await b({url:m,req:k});if(f instanceof Response)return t.error(`\u{1F6D1} Request not authorized for route handler '${n}'`),f;let y;if(e.segments){let o=await O.params;if(o===void 0)return new Response("No segments provided",{status:400});let s=h(e.segments,o);if(s.issues)return await i(s.issues);y=s.value}let S;if(e.searchParams){let o=[...m.searchParams.keys()].map(d=>{let l=m.searchParams.getAll(d);return[d,l.length>1?l:l[0]]}),s=h(e.searchParams,Object.fromEntries(o));if(s.issues)return await p(s.issues);S=s.value}let x=r.clone(),R;if(e.body){if(!["POST","PUT","PATCH"].includes(r.method))return new Response("Invalid method for request body",{status:405});let o;try{o=await F(x)}catch(d){return await u(d)}let s=I(e.body,o,"Request body validation must be synchronous");if(s.issues)return await P(s.issues);R=s.value}let T;if(e.formData){if(!["POST","PUT","PATCH"].includes(r.method))return new Response("Invalid method for request form data",{status:405});let o=r.headers.get("content-type");if(!_optionalChain([o, 'optionalAccess', _6 => _6.startsWith, 'call', _7 => _7("multipart/form-data")])&&!_optionalChain([o, 'optionalAccess', _8 => _8.startsWith, 'call', _9 => _9("application/x-www-form-urlencoded")]))return new Response("Invalid content type for request form data",{status:415});let s;try{s=await x.formData()}catch(l){return await u(l)}let d=h(e.formData,Object.fromEntries(s.entries()));if(d.issues)return await v(d.issues);T=d.value}let V={id:n,url:m,...f?{auth:f}:{},...y?{segments:y}:{},...S?{searchParams:S}:{},...R?{body:R}:{},...T?{formData:T}:{}};try{let o=await a(V,r);return c.stop(),t.info(`\u2705 Route handler '${n}' executed successfully in ${c.get()}`),o}catch(o){return c.stop(),t.error(`\u{1F6D1} Route handler '${n} failed to execute after ${c.get()}'`),await u(o)}}}exports.createSafeRouteHandler = H;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _chunkUDCVGQMHcjs = require('./chunk-UDCVGQMH.cjs');var E="[unknown:route:handler]",H=async e=>{if(_optionalChain([e, 'access', _ => _.headers, 'access', _2 => _2.get, 'call', _3 => _3("content-type"), 'optionalAccess', _4 => _4.startsWith, 'call', _5 => _5("application/json")]))return await e.json();let n=await e.text();return JSON.parse(n)};function B(e,p){if(e.body&&e.formData)throw new Error("You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.");let n=_chunkUDCVGQMHcjs.a.call(void 0, e.debug),o=_nullishCoalesce(e.id, () => (E)),y=_nullishCoalesce(e.onErrorResponse, () => ((t=>(n.error(`\u{1F6D1} Unexpected error in route handler '${o}'`,t),new Response("Internal server error",{status:500}))))),P=_nullishCoalesce(e.onSegmentsValidationErrorResponse, () => ((t=>(n.error(`\u{1F6D1} Invalid segments for route handler '${o}':`,t),new Response("Invalid segments",{status:400}))))),x=_nullishCoalesce(e.onSearchParamsValidationErrorResponse, () => ((t=>(n.error(`\u{1F6D1} Invalid search params for route handler '${o}':`,t),new Response("Invalid search params",{status:400}))))),g=_nullishCoalesce(e.onBodyValidationErrorResponse, () => ((t=>(n.error(`\u{1F6D1} Invalid body for route handler '${o}':`,t),new Response("Invalid body",{status:400}))))),v=_nullishCoalesce(e.onFormDataValidationErrorResponse, () => ((t=>(n.error(`\u{1F6D1} Invalid form data for route handler '${o}':`,t),new Response("Invalid form data",{status:400}))))),A=_nullishCoalesce(e.authorize, () => ((async()=>{})));return async function(t,b){let d=_chunkUDCVGQMHcjs.b.call(void 0, );d.start(),n.info(`\u{1F504} Running route handler '${o}'`),n.info(`\u{1F449}\u{1F3FB} Request ${t.method} ${t.url}`);let f=new URL(t.url),u;if(e.segments){let a=await b.params;if(a===void 0)return new Response("No segments provided",{status:400});let r=_chunkUDCVGQMHcjs.d.call(void 0, e.segments,a);if(r.issues)return await P(r.issues);u=r.value}let i;if(e.searchParams){let a=[...f.searchParams.keys()].map(s=>{let l=f.searchParams.getAll(s);return[s,l.length>1?l:l[0]]}),r=_chunkUDCVGQMHcjs.d.call(void 0, e.searchParams,Object.fromEntries(a));if(r.issues)return await x(r.issues);i=r.value}let S=t.clone(),c;if(e.body){if(!["POST","PUT","PATCH"].includes(t.method))return new Response("Invalid method for request body",{status:405});let a;try{a=await H(S)}catch(s){return await y(s)}let r=_chunkUDCVGQMHcjs.c.call(void 0, e.body,a,"Request body validation must be synchronous");if(r.issues)return await g(r.issues);c=r.value}let m;if(e.formData){if(!["POST","PUT","PATCH"].includes(t.method))return new Response("Invalid method for request form data",{status:405});let a=t.headers.get("content-type");if(!_optionalChain([a, 'optionalAccess', _6 => _6.startsWith, 'call', _7 => _7("multipart/form-data")])&&!_optionalChain([a, 'optionalAccess', _8 => _8.startsWith, 'call', _9 => _9("application/x-www-form-urlencoded")]))return new Response("Invalid content type for request form data",{status:415});let r;try{r=await S.formData()}catch(l){return await y(l)}let s=_chunkUDCVGQMHcjs.d.call(void 0, e.formData,Object.fromEntries(r.entries()));if(s.issues)return await v(s.issues);m=s.value}let C=t.clone(),F={id:o,url:f,req:C,...u?{segments:u}:{},...i?{searchParams:i}:{},...c?{body:c}:{},...m?{formData:m}:{}},h=await A(F);if(h instanceof Response)return n.error(`\u{1F6D1} Request not authorized for route handler '${o}'`),h;let I={id:o,url:f,...h?{auth:h}:{},...u?{segments:u}:{},...i?{searchParams:i}:{},...c?{body:c}:{},...m?{formData:m}:{}};try{let a=await p(I,t);return d.stop(),n.info(`\u2705 Route handler '${o}' executed successfully in ${d.get()}`),a}catch(a){return d.stop(),n.error(`\u{1F6D1} Route handler '${o} failed to execute after ${d.get()}'`),await y(a)}}}exports.createSafeRouteHandler = B;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts"],"names":["isPromise","value","assertsSyncOperation","message","createLogger","debug","shouldLog","rest","createExecutionClock","startTime","endTime"],"mappings":"AAAO,0rBAASA,CAAAA,CACdC,CAAAA,CACsC,CACtC,OACEA,CAAAA,GAAU,IAAA,EAAA,CACT,OAAOA,CAAAA,EAAU,QAAA,EAAY,OAAOA,CAAAA,EAAU,UAAA,CAAA,EAE/C,OAAQA,CAAAA,CAAc,IAAA,EAAS,UAEnC,CAEO,SAASC,CAAAA,CACdD,CAAAA,CACAE,CAAAA,CACoB,CACpB,EAAA,CAAIH,CAAAA,CAAaC,CAAK,CAAA,CACpB,MAAM,IAAI,KAAA,CAAME,CAAO,CAE3B,CAEO,SAASC,CAAAA,CAAaC,CAAAA,CAAiB,CAAA,CAAA,CAAO,CACnD,IAAMC,CAAAA,CAAYD,CAAAA,EAAS,OAAA,CAAQ,GAAA,CAAI,QAAA,GAAa,YAAA,CACpD,MAAO,CACL,IAAA,CAAM,CAACF,CAAAA,CAAAA,GAAoBI,CAAAA,CAAAA,EAA0B,CAC/CD,CAAAA,EACF,OAAA,CAAQ,GAAA,CAAIH,CAAAA,CAAS,GAAGI,CAAI,CAEhC,CAAA,CACA,KAAA,CAAO,CAACJ,CAAAA,CAAAA,GAAoBI,CAAAA,CAAAA,EAA0B,CAChDD,CAAAA,EACF,OAAA,CAAQ,KAAA,CAAMH,CAAAA,CAAS,GAAGI,CAAI,CAElC,CAAA,CACA,IAAA,CAAM,CAACJ,CAAAA,CAAAA,GAAoBI,CAAAA,CAAAA,EAA0B,CAC/CD,CAAAA,EACF,OAAA,CAAQ,IAAA,CAAKH,CAAAA,CAAS,GAAGI,CAAI,CAEjC,CACF,CACF,CAEO,SAASC,CAAAA,CAAAA,CAAuB,CACrC,IAAIC,CAAAA,CAA2B,IAAA,CAC3BC,CAAAA,CAAyB,IAAA,CAE7B,MAAO,CACL,KAAA,CAAO,CAAA,CAAA,EAAY,CACjBD,CAAAA,CAAY,WAAA,CAAY,GAAA,CAAI,CAC9B,CAAA,CACA,IAAA,CAAM,CAAA,CAAA,EAAY,CAChB,EAAA,CAAIA,CAAAA,GAAc,IAAA,CAChB,MAAM,IAAI,KAAA,CAAM,kCAAkC,CAAA,CAEpDC,CAAAA,CAAU,WAAA,CAAY,GAAA,CAAI,CAC5B,CAAA,CACA,GAAA,CAAK,CAAA,CAAA,EAAc,CACjB,EAAA,CAAI,CAACD,CAAAA,EAAa,CAACC,CAAAA,CACjB,MAAM,IAAI,KAAA,CAAM,kDAAkD,CAAA,CAIpE,MAAO,CAAA,EAAA","file":"/home/runner/work/anzen/anzen/dist/index.cjs","sourcesContent":["export function isPromise<T>(\n value: unknown\n): value is Promise<T> | PromiseLike<T> {\n return (\n value !== null &&\n (typeof value === 'object' || typeof value === 'function') &&\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n typeof (value as any).then === 'function'\n )\n}\n\nexport function assertsSyncOperation<T>(\n value: T | Promise<T> | PromiseLike<T>,\n message: string\n): asserts value is T {\n if (isPromise<T>(value)) {\n throw new Error(message)\n }\n}\n\nexport function createLogger(debug: boolean = false) {\n const shouldLog = debug || process.env.NODE_ENV !== 'production'\n return {\n info: (message: string, ...rest: unknown[]): void => {\n if (shouldLog) {\n console.log(message, ...rest)\n }\n },\n error: (message: string, ...rest: unknown[]): void => {\n if (shouldLog) {\n console.error(message, ...rest)\n }\n },\n warn: (message: string, ...rest: unknown[]): void => {\n if (shouldLog) {\n console.warn(message, ...rest)\n }\n },\n }\n}\n\nexport function createExecutionClock() {\n let startTime: number | null = null\n let endTime: number | null = null\n\n return {\n start: (): void => {\n startTime = performance.now()\n },\n stop: (): void => {\n if (startTime === null) {\n throw new Error('Execution clock was not started.')\n }\n endTime = performance.now()\n },\n get: (): string => {\n if (!startTime || !endTime) {\n throw new Error('Execution clock has not been started or stopped.')\n }\n\n const duration = endTime - startTime\n return `${duration.toFixed(2)}ms`\n },\n }\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/anzen/anzen/dist/index.cjs","../src/create-safe-route-handler.ts"],"names":["DEFAULT_ID","readRequestBodyAsJson","req","text","createSafeRouteHandler","options","handlerFn","log","createLogger","id","onErrorResponse","err","onSegmentsValidationErrorResponse","issues","onSearchParamsValidationErrorResponse","onBodyValidationErrorResponse","onFormDataValidationErrorResponse","authorize","providedContext","executionClock","createExecutionClock"],"mappings":"AAAA,yuBAA6D,ICsBhDA,CAAAA,CAAa,yBAAA,CAQpBC,CAAAA,CAAwB,MAC5BC,CAAAA,EACqB,CAErB,EAAA,iBADoBA,CAAAA,mBAAI,OAAA,qBAAQ,GAAA,mBAAI,cAAc,CAAA,6BACjC,UAAA,mBAAW,kBAAkB,GAAA,CAC5C,OAAO,MAAMA,CAAAA,CAAI,IAAA,CAAK,CAAA,CAGxB,IAAMC,CAAAA,CAAO,MAAMD,CAAAA,CAAI,IAAA,CAAK,CAAA,CAC5B,OAAO,IAAA,CAAK,KAAA,CAAMC,CAAI,CACxB,CAAA,CAsCO,SAASC,CAAAA,CAQdC,CAAAA,CAOAC,CAAAA,CAQwC,CAExC,EAAA,CAAID,CAAAA,CAAQ,IAAA,EAAQA,CAAAA,CAAQ,QAAA,CAC1B,MAAM,IAAI,KAAA,CACR,wGACF,CAAA,CAGF,IAAME,CAAAA,CAAMC,iCAAAA,CAAaH,CAAQ,KAAK,CAAA,CAChCI,CAAAA,kBAAKJ,CAAAA,CAAQ,EAAA,SAAML,GAAAA,CAEnBU,CAAAA,kBACJL,CAAAA,CAAQ,eAAA,SAAA,CACNM,CAAAA,EAAAA,CACAJ,CAAAA,CAAI,KAAA,CAAM,CAAA,6CAAA,EAAyCE,CAAE,CAAA,CAAA,CAAA,CAAKE,CAAG,CAAA,CACtD,IAAI,QAAA,CAAS,uBAAA,CAAyB,CAC3C,MAAA,CAAQ,GACV,CAAC,CAAA,CAAA,GAAA,CAGCC,CAAAA,kBACJP,CAAAA,CAAQ,iCAAA,SAAA,CACNQ,CAAAA,EAAAA,CACAN,CAAAA,CAAI,KAAA,CAAM,CAAA,8CAAA,EAA0CE,CAAE,CAAA,EAAA,CAAA,CAAMI,CAAM,CAAA,CAC3D,IAAI,QAAA,CAAS,kBAAA,CAAoB,CACtC,MAAA,CAAQ,GACV,CAAC,CAAA,CAAA,GAAA,CAGCC,CAAAA,kBACJT,CAAAA,CAAQ,qCAAA,SAAA,CACNQ,CAAAA,EAAAA,CACAN,CAAAA,CAAI,KAAA,CAAM,CAAA,mDAAA,EAA+CE,CAAE,CAAA,EAAA,CAAA,CAAMI,CAAM,CAAA,CAChE,IAAI,QAAA,CAAS,uBAAA,CAAyB,CAC3C,MAAA,CAAQ,GACV,CAAC,CAAA,CAAA,GAAA,CAGCE,CAAAA,kBACJV,CAAAA,CAAQ,6BAAA,SAAA,CACNQ,CAAAA,EAAAA,CACAN,CAAAA,CAAI,KAAA,CAAM,CAAA,0CAAA,EAAsCE,CAAE,CAAA,EAAA,CAAA,CAAMI,CAAM,CAAA,CACvD,IAAI,QAAA,CAAS,cAAA,CAAgB,CAClC,MAAA,CAAQ,GACV,CAAC,CAAA,CAAA,GAAA,CAGCG,CAAAA,kBACJX,CAAAA,CAAQ,iCAAA,SAAA,CACNQ,CAAAA,EAAAA,CACAN,CAAAA,CAAI,KAAA,CAAM,CAAA,+CAAA,EAA2CE,CAAE,CAAA,EAAA,CAAA,CAAMI,CAAM,CAAA,CAC5D,IAAI,QAAA,CAAS,mBAAA,CAAqB,CACvC,MAAA,CAAQ,GACV,CAAC,CAAA,CAAA,GAAA,CAGCI,CAAAA,kBAAYZ,CAAAA,CAAQ,SAAA,SAAA,CAAc,KAAA,CAAA,CAAA,EAAS,CAAA,CAAA,GAAA,CAGjD,OAAO,MAAA,QAAA,CACLH,CAAAA,CACAgB,CAAAA,CACmB,CACnB,IAAMC,CAAAA,CAAiBC,iCAAAA,CAAqB,CAC5CD,CAAAA,CAAe,KAAA,CAAM,CAAA,CAErBZ,CAAAA,CAAI,IAAA,CAAK,CAAA,iCAAA,EAA6BE,CAAE,CAAA,CAAA,CAAG,CAAA,CAC3CF,CAAAA,CAAI,IAAA,CAAK,CAAA,2BAAA,EAAgBL,CAAAA,CAAI,MAAM,CAAA,CAAA,EAAIA,CAAAA,CAAI,GAAG,CAAA,CAAA","file":"/home/runner/work/anzen/anzen/dist/index.cjs","sourcesContent":[null,"import { createLogger, createExecutionClock } from './utils'\nimport {\n parseWithDictionary,\n validateWithSchema,\n type StandardSchemaV1,\n} from './standard-schema'\nimport type {\n Awaitable,\n AuthContext,\n TSegmentsDict,\n TSearchParamsDict,\n TBodySchema,\n TFormDataDict,\n ProvidedRouteContext,\n CreateSafeRouteHandlerOptions,\n CreateSafeRouteHandlerReturnType,\n SafeRouteHandler,\n SafeRouteHandlerContext,\n AuthFunctionParams,\n} from './types'\n\n/** @internal exported for testing only */\nexport const DEFAULT_ID = '[unknown:route:handler]'\n\n/**\n * @internal\n *\n * Reads the request body as JSON.\n * If it fails, it calls the `onErrorResponse` callback.\n */\nconst readRequestBodyAsJson = async <TReq extends Request>(\n req: TReq\n): Promise<unknown> => {\n const contentType = req.headers.get('content-type')\n if (contentType?.startsWith('application/json')) {\n return await req.json()\n }\n\n const text = await req.text()\n return JSON.parse(text)\n}\n\n/**\n * Creates a safe route handler with data validation and error handling\n * for Next.js (>= 14) API route handlers.\n *\n * @param options - Options to configure the route handler.\n * @param handlerFn - The route handler function.\n *\n * @returns A Next.js API route handler function.\n *\n * @example\n * ```ts\n * import { string } from 'decoders'\n *import { createSafeRouteHandler } from '@sugardarius/anzen'\n * import { auth } from '~/lib/auth'\n *\n * export const GET = createSafeRouteHandler(\n * {\n * id: 'my-safe-route-handler',\n * authorize: async ({ req }) => {\n * const session = await auth.getSession(req)\n * if (!session) {\n * return new Response(null, { status: 401 })\n * }\n *\n * return { user: session.user }\n * },\n * segments: {\n * id: string,\n * },\n * },\n * async ({ auth, segments, }, req): Promise<Response> => {\n * return Response.json({ user: auth.user, segments }, { status: 200 })\n * }\n *)\n * ```\n */\nexport function createSafeRouteHandler<\n AC extends AuthContext | undefined = undefined,\n TRouteDynamicSegments extends TSegmentsDict | undefined = undefined,\n TSearchParams extends TSearchParamsDict | undefined = undefined,\n TBody extends TBodySchema | undefined = undefined,\n TFormData extends TFormDataDict | undefined = undefined,\n TReq extends Request = Request,\n>(\n options: CreateSafeRouteHandlerOptions<\n AC,\n TRouteDynamicSegments,\n TSearchParams,\n TBody,\n TFormData\n >,\n handlerFn: SafeRouteHandler<\n AC,\n TRouteDynamicSegments,\n TSearchParams,\n TBody,\n TFormData,\n TReq\n >\n): CreateSafeRouteHandlerReturnType<TReq> {\n // NOTE: `body` and `formData` options are mutually exclusive 🎭\n if (options.body && options.formData) {\n throw new Error(\n 'You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.'\n )\n }\n\n const log = createLogger(options.debug)\n const id = options.id ?? DEFAULT_ID\n\n const onErrorResponse =\n options.onErrorResponse ??\n ((err: unknown): Awaitable<Response> => {\n log.error(`🛑 Unexpected error in route handler '${id}'`, err)\n return new Response('Internal server error', {\n status: 500,\n })\n })\n\n const onSegmentsValidationErrorResponse =\n options.onSegmentsValidationErrorResponse ??\n ((issues: readonly StandardSchemaV1.Issue[]): Awaitable<Response> => {\n log.error(`🛑 Invalid segments for route handler '${id}':`, issues)\n return new Response('Invalid segments', {\n status: 400,\n })\n })\n\n const onSearchParamsValidationErrorResponse =\n options.onSearchParamsValidationErrorResponse ??\n ((issues: readonly StandardSchemaV1.Issue[]): Awaitable<Response> => {\n log.error(`🛑 Invalid search params for route handler '${id}':`, issues)\n return new Response('Invalid search params', {\n status: 400,\n })\n })\n\n const onBodyValidationErrorResponse =\n options.onBodyValidationErrorResponse ??\n ((issues: readonly StandardSchemaV1.Issue[]): Awaitable<Response> => {\n log.error(`🛑 Invalid body for route handler '${id}':`, issues)\n return new Response('Invalid body', {\n status: 400,\n })\n })\n\n const onFormDataValidationErrorResponse =\n options.onFormDataValidationErrorResponse ??\n ((issues: readonly StandardSchemaV1.Issue[]): Awaitable<Response> => {\n log.error(`🛑 Invalid form data for route handler '${id}':`, issues)\n return new Response('Invalid form data', {\n status: 400,\n })\n })\n\n const authorize = options.authorize ?? (async () => undefined)\n\n // Next.js API Route handler declaration\n return async function (\n req: TReq,\n providedContext: ProvidedRouteContext\n ): Promise<Response> {\n const executionClock = createExecutionClock()\n executionClock.start()\n\n log.info(`🔄 Running route handler '${id}'`)\n log.info(`👉🏻 Request ${req.method} ${req.url}`)\n\n const url = new URL(req.url)\n\n let segments = undefined\n if (options.segments) {\n const params = await providedContext.params\n if (params === undefined) {\n return new Response('No segments provided', { status: 400 })\n }\n\n const parsedSegments = parseWithDictionary(options.segments, params)\n if (parsedSegments.issues) {\n return await onSegmentsValidationErrorResponse(parsedSegments.issues)\n }\n\n segments = parsedSegments.value\n }\n\n let searchParams = undefined\n if (options.searchParams) {\n const queryParams_unsafe = [...url.searchParams.keys()].map((k) => {\n const values = url.searchParams.getAll(k)\n return [k, values.length > 1 ? values : values[0]]\n })\n\n const parsedSearchParams = parseWithDictionary(\n options.searchParams,\n Object.fromEntries(queryParams_unsafe)\n )\n\n if (parsedSearchParams.issues) {\n return await onSearchParamsValidationErrorResponse(\n parsedSearchParams.issues\n )\n }\n\n searchParams = parsedSearchParams.value\n }\n\n // Do not mutate / consume the original request\n const clonedReq_forBody = req.clone() as TReq\n\n let body = undefined\n if (options.body) {\n if (!['POST', 'PUT', 'PATCH'].includes(req.method)) {\n return new Response('Invalid method for request body', {\n status: 405,\n })\n }\n\n let body_unsafe: unknown\n try {\n body_unsafe = await readRequestBodyAsJson(clonedReq_forBody)\n } catch (err) {\n return await onErrorResponse(err)\n }\n\n const parsedBody = validateWithSchema(\n options.body,\n body_unsafe,\n 'Request body validation must be synchronous'\n )\n\n if (parsedBody.issues) {\n return await onBodyValidationErrorResponse(parsedBody.issues)\n }\n\n body = parsedBody.value\n }\n\n let formData = undefined\n if (options.formData) {\n if (!['POST', 'PUT', 'PATCH'].includes(req.method)) {\n return new Response('Invalid method for request form data', {\n status: 405,\n })\n }\n\n const contentType = req.headers.get('content-type')\n if (\n !contentType?.startsWith('multipart/form-data') &&\n !contentType?.startsWith('application/x-www-form-urlencoded')\n ) {\n return new Response('Invalid content type for request form data', {\n status: 415,\n })\n }\n\n let formData_unsafe: FormData\n try {\n // NOTE: 🤔 maybe find a better way to counter the deprecation warning?\n formData_unsafe = await clonedReq_forBody.formData()\n } catch (err) {\n return await onErrorResponse(err)\n }\n\n const parsedFormData = parseWithDictionary(\n options.formData,\n Object.fromEntries(formData_unsafe.entries())\n )\n\n if (parsedFormData.issues) {\n return await onFormDataValidationErrorResponse(parsedFormData.issues)\n }\n\n formData = parsedFormData.value\n }\n\n // Do not mutate / consume the original request\n // Due to `NextRequest` limitations as the req is cloned it's always a Request\n const clonedReq_forAuth = req.clone()\n const authParams = {\n id,\n url,\n req: clonedReq_forAuth,\n ...(segments ? { segments } : {}),\n ...(searchParams ? { searchParams } : {}),\n ...(body ? { body } : {}),\n ...(formData ? { formData } : {}),\n } as AuthFunctionParams<\n TRouteDynamicSegments,\n TSearchParams,\n TBody,\n TFormData\n >\n const authOrResponse = await authorize(authParams)\n if (authOrResponse instanceof Response) {\n log.error(`🛑 Request not authorized for route handler '${id}'`)\n return authOrResponse\n }\n\n // Build safe route handler context\n const ctx = {\n id,\n url,\n ...(authOrResponse ? { auth: authOrResponse } : {}),\n ...(segments ? { segments } : {}),\n ...(searchParams ? { searchParams } : {}),\n ...(body ? { body } : {}),\n ...(formData ? { formData } : {}),\n } as SafeRouteHandlerContext<\n AC,\n TRouteDynamicSegments,\n TSearchParams,\n TBody,\n TFormData\n >\n\n // Let's catch any error that might happen in the handler\n try {\n const response = await handlerFn(ctx, req)\n\n executionClock.stop()\n log.info(\n `✅ Route handler '${id}' executed successfully in ${executionClock.get()}`\n )\n\n return response\n } catch (err) {\n executionClock.stop()\n log.error(\n `🛑 Route handler '${id} failed to execute after ${executionClock.get()}'`\n )\n\n return await onErrorResponse(err)\n }\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,242 +1,5 @@
1
- /** The Standard Schema interface. */
2
- interface StandardSchemaV1<Input = unknown, Output = Input> {
3
- /** The Standard Schema properties. */
4
- readonly '~standard': StandardSchemaV1.Props<Input, Output>;
5
- }
6
- declare namespace StandardSchemaV1 {
7
- /** The Standard Schema properties interface. */
8
- interface Props<Input = unknown, Output = Input> {
9
- /** The version number of the standard. */
10
- readonly version: 1;
11
- /** The vendor name of the schema library. */
12
- readonly vendor: string;
13
- /** Validates unknown input values. */
14
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
15
- /** Inferred types associated with the schema. */
16
- readonly types?: Types<Input, Output> | undefined;
17
- }
18
- /** The result interface of the validate function. */
19
- type Result<Output> = SuccessResult<Output> | FailureResult;
20
- /** The result interface if validation succeeds. */
21
- interface SuccessResult<Output> {
22
- /** The typed output value. */
23
- readonly value: Output;
24
- /** The non-existent issues. */
25
- readonly issues?: undefined;
26
- }
27
- /** The result interface if validation fails. */
28
- interface FailureResult {
29
- /** The issues of failed validation. */
30
- readonly issues: ReadonlyArray<Issue>;
31
- }
32
- /** The issue interface of the failure output. */
33
- interface Issue {
34
- /** The error message of the issue. */
35
- readonly message: string;
36
- /** The path of the issue, if any. */
37
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
38
- }
39
- /** The path segment interface of the issue. */
40
- interface PathSegment {
41
- /** The key representing a path segment. */
42
- readonly key: PropertyKey;
43
- }
44
- /** The Standard Schema types interface. */
45
- interface Types<Input = unknown, Output = Input> {
46
- /** The input type of the schema. */
47
- readonly input: Input;
48
- /** The output type of the schema. */
49
- readonly output: Output;
50
- }
51
- /** Infers the input type of a Standard Schema. */
52
- type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
53
- /** Infers the output type of a Standard Schema. */
54
- type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
55
- }
56
- type StandardSchemaDictionary<Input = Record<string, unknown>, Output extends Record<keyof Input, unknown> = Input> = {
57
- [K in keyof Input]-?: StandardSchemaV1<Input[K], Output[K]>;
58
- };
59
- declare namespace StandardSchemaDictionary {
60
- type InferInput<T extends StandardSchemaDictionary> = {
61
- [K in keyof T]: StandardSchemaV1.InferInput<T[K]>;
62
- };
63
- type InferOutput<T extends StandardSchemaDictionary> = {
64
- [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>;
65
- };
66
- }
67
-
68
- type EmptyObjectType = {};
69
- type UnwrapReadonlyObject<T> = T extends Readonly<infer U> ? U : T;
70
- type Awaitable<T> = T | PromiseLike<T>;
71
- type AuthContext = Record<string, unknown>;
72
- type TSegmentsDict = StandardSchemaDictionary;
73
- type TSearchParamsDict = StandardSchemaDictionary;
74
- type TBodySchema = StandardSchemaV1;
75
- type TFormDataDict = StandardSchemaDictionary;
76
- type AuthFunction<AC extends AuthContext | undefined> = (input: {
77
- /**
78
- * Parsed request url
79
- */
80
- readonly url: URL;
81
- /**
82
- * Original request
83
- *
84
- * Cloned from the incoming request to avoid side effects
85
- * and to make it consumable in the `authorize` function.
86
- * Due to `NextRequest` limitations as the req is cloned it's always a `Request`
87
- */
88
- req: Request;
89
- }) => Awaitable<AC | Response>;
90
- type BaseOptions<AC extends AuthContext | undefined> = {
91
- /**
92
- * ID for the route handler.
93
- * Used when logging in development or when `debug` is enabled.
94
- *
95
- * You can also use it to add extra logging or monitoring.
96
- */
97
- id?: string;
98
- /**
99
- * Function to use to authorize the request.
100
- * By default it always authorize the request.
101
- *
102
- * When returning a response, it will be used as the response for the request.
103
- * Return a response when the request is not authorized.
104
- */
105
- authorize?: AuthFunction<AC>;
106
- /**
107
- * Callback triggered when the request fails.
108
- * By default it returns a simple `500` response and the error is logged into the console.
109
- *
110
- * Use it if your handler use custom errors and
111
- * you want to manage them properly by returning a proper response.
112
- */
113
- onErrorResponse?: (err: unknown) => Awaitable<Response>;
114
- /**
115
- * Use this options to enable debug mode.
116
- * It will add logs in the handler to help you debug the request.
117
- *
118
- * By default it's set to `false` for production builds.
119
- * In development builds, it will be `true` if `NODE_ENV` is not set to `production`.
120
- */
121
- debug?: boolean;
122
- };
123
- type OnValidationErrorResponse = (issues: readonly StandardSchemaV1.Issue[]) => Awaitable<Response>;
124
- type CreateSafeRouteHandlerOptions<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
125
- /**
126
- * Dynamic route segments used for the route handler path.
127
- * By design it will handler if the segments are a `Promise` or not.
128
- *
129
- * Please note the expected input is a `StandardSchemaDictionary`.
130
- */
131
- segments?: TSegments;
132
- /**
133
- * Callback triggered when dynamic segments validations returned issues.
134
- * By default it returns a simple `400` response and issues are logged into the console.
135
- */
136
- onSegmentsValidationErrorResponse?: OnValidationErrorResponse;
137
- /**
138
- * Search params used in the route.
139
- *
140
- * Please note the expected input is a `StandardSchemaDictionary`.
141
- */
142
- searchParams?: TSearchParams;
143
- /**
144
- * Callback triggered when search params validations returned issues.
145
- * By default it returns a simple `400` response and issues are logged into the console.
146
- */
147
- onSearchParamsValidationErrorResponse?: OnValidationErrorResponse;
148
- /**
149
- * Request body.
150
- *
151
- * Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
152
- * Returns a `415`response if the request does not explicitly set the `Content-Type` to `application/json`.
153
- *
154
- * IMPORTANT: The body is parsed as JSON, so it must be a valid JSON object!
155
- * IMPORTANT: Body shouldn't be used with `formData` at the same time. They are exclusive.
156
- * Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
157
- */
158
- body?: TBody;
159
- /**
160
- * Callback triggered when body validation returned issues.
161
- * By default it returns a simple `400` response and issues are logged into the console.
162
- */
163
- onBodyValidationErrorResponse?: OnValidationErrorResponse;
164
- /**
165
- * Request form data.
166
- *
167
- * Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
168
- * Returns a `415`response if the request does not explicitly set the `Content-Type` to `multipart/form-data`
169
- * or to `application/x-www-form-urlencoded`.
170
- *
171
- * IMPORTANT: formData shouldn't be used with `body` at the same time. They are exclusive.
172
- * Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
173
- */
174
- formData?: TFormData;
175
- /**
176
- * Callback triggered when form data validation returned issues.
177
- * By default it returns a simple `400` response and issues are logged into the console.
178
- */
179
- onFormDataValidationErrorResponse?: OnValidationErrorResponse;
180
- } & BaseOptions<AC>;
181
- type RequestExtras = {
182
- /**
183
- * Route dynamic segments as params
184
- */
185
- params: Awaitable<any> | undefined;
186
- };
187
- type CreateSafeRouteHandlerReturnType<TReq extends Request = Request> = (
188
- /**
189
- * Original request
190
- */
191
- req: TReq,
192
- /**
193
- * Extras added by Next.js itself
194
- */
195
- extras: RequestExtras) => Promise<Response>;
196
- type SafeRouteHandlerContext<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
197
- /**
198
- * Route handler ID
199
- */
200
- readonly id: string;
201
- /**
202
- * Parsed request url
203
- */
204
- readonly url: URL;
205
- } & (AC extends AuthContext ? {
206
- /**
207
- * Auth context
208
- */
209
- readonly auth: AC;
210
- } : EmptyObjectType) & (TSegments extends TSegmentsDict ? {
211
- /**
212
- * Validated route dynamic segments
213
- */
214
- readonly segments: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSegments>>;
215
- } : EmptyObjectType) & (TSearchParams extends TSearchParamsDict ? {
216
- /**
217
- * Validated search params
218
- */
219
- readonly searchParams: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSearchParams>>;
220
- } : EmptyObjectType) & (TBody extends TBodySchema ? {
221
- /**
222
- * Validated request body
223
- */
224
- readonly body: StandardSchemaV1.InferOutput<TBody>;
225
- } : EmptyObjectType) & (TFormData extends TFormDataDict ? {
226
- /**
227
- * Validated form data
228
- */
229
- readonly formData: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TFormData>>;
230
- } : EmptyObjectType);
231
- type SafeRouteHandler<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined, TReq extends Request = Request> = (
232
- /**
233
- * Safe route handler context
234
- */
235
- ctx: SafeRouteHandlerContext<AC, TSegments, TSearchParams, TBody, TFormData>,
236
- /**
237
- * Original request
238
- */
239
- req: TReq) => Promise<Response>;
1
+ import { A as AuthContext, T as TSegmentsDict, a as TSearchParamsDict, b as TBodySchema, c as TFormDataDict, C as CreateSafeRouteHandlerOptions, S as SafeRouteHandler, d as CreateSafeRouteHandlerReturnType } from './types-LPIIICMI.cjs';
2
+ export { f as AuthFunction, g as AuthFunctionParams, e as Awaitable, B as BaseOptions, O as OnValidationErrorResponse, P as ProvidedRouteContext, h as SafeRouteHandlerContext } from './types-LPIIICMI.cjs';
240
3
 
241
4
  /**
242
5
  * Creates a safe route handler with data validation and error handling
@@ -276,4 +39,4 @@ req: TReq) => Promise<Response>;
276
39
  */
277
40
  declare function createSafeRouteHandler<AC extends AuthContext | undefined = undefined, TRouteDynamicSegments extends TSegmentsDict | undefined = undefined, TSearchParams extends TSearchParamsDict | undefined = undefined, TBody extends TBodySchema | undefined = undefined, TFormData extends TFormDataDict | undefined = undefined, TReq extends Request = Request>(options: CreateSafeRouteHandlerOptions<AC, TRouteDynamicSegments, TSearchParams, TBody, TFormData>, handlerFn: SafeRouteHandler<AC, TRouteDynamicSegments, TSearchParams, TBody, TFormData, TReq>): CreateSafeRouteHandlerReturnType<TReq>;
278
41
 
279
- export { type AuthContext, type AuthFunction, type Awaitable, type BaseOptions, type CreateSafeRouteHandlerOptions, type CreateSafeRouteHandlerReturnType, type OnValidationErrorResponse, type RequestExtras, type SafeRouteHandler, type SafeRouteHandlerContext, type TBodySchema, type TFormDataDict, type TSearchParamsDict, type TSegmentsDict, createSafeRouteHandler };
42
+ export { AuthContext, CreateSafeRouteHandlerOptions, CreateSafeRouteHandlerReturnType, SafeRouteHandler, TBodySchema, TFormDataDict, TSearchParamsDict, TSegmentsDict, createSafeRouteHandler };
package/dist/index.d.ts CHANGED
@@ -1,242 +1,5 @@
1
- /** The Standard Schema interface. */
2
- interface StandardSchemaV1<Input = unknown, Output = Input> {
3
- /** The Standard Schema properties. */
4
- readonly '~standard': StandardSchemaV1.Props<Input, Output>;
5
- }
6
- declare namespace StandardSchemaV1 {
7
- /** The Standard Schema properties interface. */
8
- interface Props<Input = unknown, Output = Input> {
9
- /** The version number of the standard. */
10
- readonly version: 1;
11
- /** The vendor name of the schema library. */
12
- readonly vendor: string;
13
- /** Validates unknown input values. */
14
- readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
15
- /** Inferred types associated with the schema. */
16
- readonly types?: Types<Input, Output> | undefined;
17
- }
18
- /** The result interface of the validate function. */
19
- type Result<Output> = SuccessResult<Output> | FailureResult;
20
- /** The result interface if validation succeeds. */
21
- interface SuccessResult<Output> {
22
- /** The typed output value. */
23
- readonly value: Output;
24
- /** The non-existent issues. */
25
- readonly issues?: undefined;
26
- }
27
- /** The result interface if validation fails. */
28
- interface FailureResult {
29
- /** The issues of failed validation. */
30
- readonly issues: ReadonlyArray<Issue>;
31
- }
32
- /** The issue interface of the failure output. */
33
- interface Issue {
34
- /** The error message of the issue. */
35
- readonly message: string;
36
- /** The path of the issue, if any. */
37
- readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
38
- }
39
- /** The path segment interface of the issue. */
40
- interface PathSegment {
41
- /** The key representing a path segment. */
42
- readonly key: PropertyKey;
43
- }
44
- /** The Standard Schema types interface. */
45
- interface Types<Input = unknown, Output = Input> {
46
- /** The input type of the schema. */
47
- readonly input: Input;
48
- /** The output type of the schema. */
49
- readonly output: Output;
50
- }
51
- /** Infers the input type of a Standard Schema. */
52
- type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
53
- /** Infers the output type of a Standard Schema. */
54
- type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
55
- }
56
- type StandardSchemaDictionary<Input = Record<string, unknown>, Output extends Record<keyof Input, unknown> = Input> = {
57
- [K in keyof Input]-?: StandardSchemaV1<Input[K], Output[K]>;
58
- };
59
- declare namespace StandardSchemaDictionary {
60
- type InferInput<T extends StandardSchemaDictionary> = {
61
- [K in keyof T]: StandardSchemaV1.InferInput<T[K]>;
62
- };
63
- type InferOutput<T extends StandardSchemaDictionary> = {
64
- [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>;
65
- };
66
- }
67
-
68
- type EmptyObjectType = {};
69
- type UnwrapReadonlyObject<T> = T extends Readonly<infer U> ? U : T;
70
- type Awaitable<T> = T | PromiseLike<T>;
71
- type AuthContext = Record<string, unknown>;
72
- type TSegmentsDict = StandardSchemaDictionary;
73
- type TSearchParamsDict = StandardSchemaDictionary;
74
- type TBodySchema = StandardSchemaV1;
75
- type TFormDataDict = StandardSchemaDictionary;
76
- type AuthFunction<AC extends AuthContext | undefined> = (input: {
77
- /**
78
- * Parsed request url
79
- */
80
- readonly url: URL;
81
- /**
82
- * Original request
83
- *
84
- * Cloned from the incoming request to avoid side effects
85
- * and to make it consumable in the `authorize` function.
86
- * Due to `NextRequest` limitations as the req is cloned it's always a `Request`
87
- */
88
- req: Request;
89
- }) => Awaitable<AC | Response>;
90
- type BaseOptions<AC extends AuthContext | undefined> = {
91
- /**
92
- * ID for the route handler.
93
- * Used when logging in development or when `debug` is enabled.
94
- *
95
- * You can also use it to add extra logging or monitoring.
96
- */
97
- id?: string;
98
- /**
99
- * Function to use to authorize the request.
100
- * By default it always authorize the request.
101
- *
102
- * When returning a response, it will be used as the response for the request.
103
- * Return a response when the request is not authorized.
104
- */
105
- authorize?: AuthFunction<AC>;
106
- /**
107
- * Callback triggered when the request fails.
108
- * By default it returns a simple `500` response and the error is logged into the console.
109
- *
110
- * Use it if your handler use custom errors and
111
- * you want to manage them properly by returning a proper response.
112
- */
113
- onErrorResponse?: (err: unknown) => Awaitable<Response>;
114
- /**
115
- * Use this options to enable debug mode.
116
- * It will add logs in the handler to help you debug the request.
117
- *
118
- * By default it's set to `false` for production builds.
119
- * In development builds, it will be `true` if `NODE_ENV` is not set to `production`.
120
- */
121
- debug?: boolean;
122
- };
123
- type OnValidationErrorResponse = (issues: readonly StandardSchemaV1.Issue[]) => Awaitable<Response>;
124
- type CreateSafeRouteHandlerOptions<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
125
- /**
126
- * Dynamic route segments used for the route handler path.
127
- * By design it will handler if the segments are a `Promise` or not.
128
- *
129
- * Please note the expected input is a `StandardSchemaDictionary`.
130
- */
131
- segments?: TSegments;
132
- /**
133
- * Callback triggered when dynamic segments validations returned issues.
134
- * By default it returns a simple `400` response and issues are logged into the console.
135
- */
136
- onSegmentsValidationErrorResponse?: OnValidationErrorResponse;
137
- /**
138
- * Search params used in the route.
139
- *
140
- * Please note the expected input is a `StandardSchemaDictionary`.
141
- */
142
- searchParams?: TSearchParams;
143
- /**
144
- * Callback triggered when search params validations returned issues.
145
- * By default it returns a simple `400` response and issues are logged into the console.
146
- */
147
- onSearchParamsValidationErrorResponse?: OnValidationErrorResponse;
148
- /**
149
- * Request body.
150
- *
151
- * Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
152
- * Returns a `415`response if the request does not explicitly set the `Content-Type` to `application/json`.
153
- *
154
- * IMPORTANT: The body is parsed as JSON, so it must be a valid JSON object!
155
- * IMPORTANT: Body shouldn't be used with `formData` at the same time. They are exclusive.
156
- * Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
157
- */
158
- body?: TBody;
159
- /**
160
- * Callback triggered when body validation returned issues.
161
- * By default it returns a simple `400` response and issues are logged into the console.
162
- */
163
- onBodyValidationErrorResponse?: OnValidationErrorResponse;
164
- /**
165
- * Request form data.
166
- *
167
- * Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
168
- * Returns a `415`response if the request does not explicitly set the `Content-Type` to `multipart/form-data`
169
- * or to `application/x-www-form-urlencoded`.
170
- *
171
- * IMPORTANT: formData shouldn't be used with `body` at the same time. They are exclusive.
172
- * Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
173
- */
174
- formData?: TFormData;
175
- /**
176
- * Callback triggered when form data validation returned issues.
177
- * By default it returns a simple `400` response and issues are logged into the console.
178
- */
179
- onFormDataValidationErrorResponse?: OnValidationErrorResponse;
180
- } & BaseOptions<AC>;
181
- type RequestExtras = {
182
- /**
183
- * Route dynamic segments as params
184
- */
185
- params: Awaitable<any> | undefined;
186
- };
187
- type CreateSafeRouteHandlerReturnType<TReq extends Request = Request> = (
188
- /**
189
- * Original request
190
- */
191
- req: TReq,
192
- /**
193
- * Extras added by Next.js itself
194
- */
195
- extras: RequestExtras) => Promise<Response>;
196
- type SafeRouteHandlerContext<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
197
- /**
198
- * Route handler ID
199
- */
200
- readonly id: string;
201
- /**
202
- * Parsed request url
203
- */
204
- readonly url: URL;
205
- } & (AC extends AuthContext ? {
206
- /**
207
- * Auth context
208
- */
209
- readonly auth: AC;
210
- } : EmptyObjectType) & (TSegments extends TSegmentsDict ? {
211
- /**
212
- * Validated route dynamic segments
213
- */
214
- readonly segments: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSegments>>;
215
- } : EmptyObjectType) & (TSearchParams extends TSearchParamsDict ? {
216
- /**
217
- * Validated search params
218
- */
219
- readonly searchParams: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSearchParams>>;
220
- } : EmptyObjectType) & (TBody extends TBodySchema ? {
221
- /**
222
- * Validated request body
223
- */
224
- readonly body: StandardSchemaV1.InferOutput<TBody>;
225
- } : EmptyObjectType) & (TFormData extends TFormDataDict ? {
226
- /**
227
- * Validated form data
228
- */
229
- readonly formData: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TFormData>>;
230
- } : EmptyObjectType);
231
- type SafeRouteHandler<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined, TReq extends Request = Request> = (
232
- /**
233
- * Safe route handler context
234
- */
235
- ctx: SafeRouteHandlerContext<AC, TSegments, TSearchParams, TBody, TFormData>,
236
- /**
237
- * Original request
238
- */
239
- req: TReq) => Promise<Response>;
1
+ import { A as AuthContext, T as TSegmentsDict, a as TSearchParamsDict, b as TBodySchema, c as TFormDataDict, C as CreateSafeRouteHandlerOptions, S as SafeRouteHandler, d as CreateSafeRouteHandlerReturnType } from './types-LPIIICMI.js';
2
+ export { f as AuthFunction, g as AuthFunctionParams, e as Awaitable, B as BaseOptions, O as OnValidationErrorResponse, P as ProvidedRouteContext, h as SafeRouteHandlerContext } from './types-LPIIICMI.js';
240
3
 
241
4
  /**
242
5
  * Creates a safe route handler with data validation and error handling
@@ -276,4 +39,4 @@ req: TReq) => Promise<Response>;
276
39
  */
277
40
  declare function createSafeRouteHandler<AC extends AuthContext | undefined = undefined, TRouteDynamicSegments extends TSegmentsDict | undefined = undefined, TSearchParams extends TSearchParamsDict | undefined = undefined, TBody extends TBodySchema | undefined = undefined, TFormData extends TFormDataDict | undefined = undefined, TReq extends Request = Request>(options: CreateSafeRouteHandlerOptions<AC, TRouteDynamicSegments, TSearchParams, TBody, TFormData>, handlerFn: SafeRouteHandler<AC, TRouteDynamicSegments, TSearchParams, TBody, TFormData, TReq>): CreateSafeRouteHandlerReturnType<TReq>;
278
41
 
279
- export { type AuthContext, type AuthFunction, type Awaitable, type BaseOptions, type CreateSafeRouteHandlerOptions, type CreateSafeRouteHandlerReturnType, type OnValidationErrorResponse, type RequestExtras, type SafeRouteHandler, type SafeRouteHandlerContext, type TBodySchema, type TFormDataDict, type TSearchParamsDict, type TSegmentsDict, createSafeRouteHandler };
42
+ export { AuthContext, CreateSafeRouteHandlerOptions, CreateSafeRouteHandlerReturnType, SafeRouteHandler, TBodySchema, TFormDataDict, TSearchParamsDict, TSegmentsDict, createSafeRouteHandler };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- function E(e){return e!==null&&(typeof e=="object"||typeof e=="function")&&typeof e.then=="function"}function w(e,a){if(E(e))throw new Error(a)}function g(e=!1){let a=e||process.env.NODE_ENV!=="production";return{info:(t,...n)=>{a&&console.log(t,...n)},error:(t,...n)=>{a&&console.error(t,...n)},warn:(t,...n)=>{a&&console.warn(t,...n)}}}function D(){let e=null,a=null;return{start:()=>{e=performance.now()},stop:()=>{if(e===null)throw new Error("Execution clock was not started.");a=performance.now()},get:()=>{if(!e||!a)throw new Error("Execution clock has not been started or stopped.");return`${(a-e).toFixed(2)}ms`}}}function I(e,a,t="Validation must be synchronous but schema returned a Promise."){let n=e["~standard"].validate(a);return w(n,t),n}var A=(e,a)=>a in e;function h(e,a){let t={},n=[];for(let u in e){if(!A(e,u))continue;let i=e[u]["~standard"].validate(a[u]);if(w(i,`Validation must be synchronous, but ${u} returned a Promise.`),i.issues){n.push(...i.issues.map(p=>({...p,path:[u,...p.path??[]]})));continue}t[u]=i.value}return n.length>0?{issues:n}:{value:t}}var C="[unknown:route:handler]",F=async e=>{if(e.headers.get("content-type")?.startsWith("application/json"))return await e.json();let t=await e.text();return JSON.parse(t)};function H(e,a){if(e.body&&e.formData)throw new Error("You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.");let t=g(e.debug),n=e.id??C,u=e.onErrorResponse??(r=>(t.error(`\u{1F6D1} Unexpected error in route handler '${n}'`,r),new Response("Internal server error",{status:500}))),i=e.onSegmentsValidationErrorResponse??(r=>(t.error(`\u{1F6D1} Invalid segments for route handler '${n}':`,r),new Response("Invalid segments",{status:400}))),p=e.onSearchParamsValidationErrorResponse??(r=>(t.error(`\u{1F6D1} Invalid search params for route handler '${n}':`,r),new Response("Invalid search params",{status:400}))),P=e.onBodyValidationErrorResponse??(r=>(t.error(`\u{1F6D1} Invalid body for route handler '${n}':`,r),new Response("Invalid body",{status:400}))),v=e.onFormDataValidationErrorResponse??(r=>(t.error(`\u{1F6D1} Invalid form data for route handler '${n}':`,r),new Response("Invalid form data",{status:400}))),b=e.authorize??(async()=>{});return async function(r,O){let c=D();c.start(),t.info(`\u{1F504} Running route handler '${n}'`),t.info(`\u{1F449}\u{1F3FB} Request ${r.method} ${r.url}`);let m=new URL(r.url),k=r.clone(),f=await b({url:m,req:k});if(f instanceof Response)return t.error(`\u{1F6D1} Request not authorized for route handler '${n}'`),f;let y;if(e.segments){let o=await O.params;if(o===void 0)return new Response("No segments provided",{status:400});let s=h(e.segments,o);if(s.issues)return await i(s.issues);y=s.value}let S;if(e.searchParams){let o=[...m.searchParams.keys()].map(d=>{let l=m.searchParams.getAll(d);return[d,l.length>1?l:l[0]]}),s=h(e.searchParams,Object.fromEntries(o));if(s.issues)return await p(s.issues);S=s.value}let x=r.clone(),R;if(e.body){if(!["POST","PUT","PATCH"].includes(r.method))return new Response("Invalid method for request body",{status:405});let o;try{o=await F(x)}catch(d){return await u(d)}let s=I(e.body,o,"Request body validation must be synchronous");if(s.issues)return await P(s.issues);R=s.value}let T;if(e.formData){if(!["POST","PUT","PATCH"].includes(r.method))return new Response("Invalid method for request form data",{status:405});let o=r.headers.get("content-type");if(!o?.startsWith("multipart/form-data")&&!o?.startsWith("application/x-www-form-urlencoded"))return new Response("Invalid content type for request form data",{status:415});let s;try{s=await x.formData()}catch(l){return await u(l)}let d=h(e.formData,Object.fromEntries(s.entries()));if(d.issues)return await v(d.issues);T=d.value}let V={id:n,url:m,...f?{auth:f}:{},...y?{segments:y}:{},...S?{searchParams:S}:{},...R?{body:R}:{},...T?{formData:T}:{}};try{let o=await a(V,r);return c.stop(),t.info(`\u2705 Route handler '${n}' executed successfully in ${c.get()}`),o}catch(o){return c.stop(),t.error(`\u{1F6D1} Route handler '${n} failed to execute after ${c.get()}'`),await u(o)}}}export{H as createSafeRouteHandler};
1
+ import{a as T,b as w,c as D,d as R}from"./chunk-DT3TEL5X.js";var E="[unknown:route:handler]",H=async e=>{if(e.headers.get("content-type")?.startsWith("application/json"))return await e.json();let n=await e.text();return JSON.parse(n)};function B(e,p){if(e.body&&e.formData)throw new Error("You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.");let n=T(e.debug),o=e.id??E,y=e.onErrorResponse??(t=>(n.error(`\u{1F6D1} Unexpected error in route handler '${o}'`,t),new Response("Internal server error",{status:500}))),P=e.onSegmentsValidationErrorResponse??(t=>(n.error(`\u{1F6D1} Invalid segments for route handler '${o}':`,t),new Response("Invalid segments",{status:400}))),x=e.onSearchParamsValidationErrorResponse??(t=>(n.error(`\u{1F6D1} Invalid search params for route handler '${o}':`,t),new Response("Invalid search params",{status:400}))),g=e.onBodyValidationErrorResponse??(t=>(n.error(`\u{1F6D1} Invalid body for route handler '${o}':`,t),new Response("Invalid body",{status:400}))),v=e.onFormDataValidationErrorResponse??(t=>(n.error(`\u{1F6D1} Invalid form data for route handler '${o}':`,t),new Response("Invalid form data",{status:400}))),A=e.authorize??(async()=>{});return async function(t,b){let d=w();d.start(),n.info(`\u{1F504} Running route handler '${o}'`),n.info(`\u{1F449}\u{1F3FB} Request ${t.method} ${t.url}`);let f=new URL(t.url),u;if(e.segments){let a=await b.params;if(a===void 0)return new Response("No segments provided",{status:400});let r=R(e.segments,a);if(r.issues)return await P(r.issues);u=r.value}let i;if(e.searchParams){let a=[...f.searchParams.keys()].map(s=>{let l=f.searchParams.getAll(s);return[s,l.length>1?l:l[0]]}),r=R(e.searchParams,Object.fromEntries(a));if(r.issues)return await x(r.issues);i=r.value}let S=t.clone(),c;if(e.body){if(!["POST","PUT","PATCH"].includes(t.method))return new Response("Invalid method for request body",{status:405});let a;try{a=await H(S)}catch(s){return await y(s)}let r=D(e.body,a,"Request body validation must be synchronous");if(r.issues)return await g(r.issues);c=r.value}let m;if(e.formData){if(!["POST","PUT","PATCH"].includes(t.method))return new Response("Invalid method for request form data",{status:405});let a=t.headers.get("content-type");if(!a?.startsWith("multipart/form-data")&&!a?.startsWith("application/x-www-form-urlencoded"))return new Response("Invalid content type for request form data",{status:415});let r;try{r=await S.formData()}catch(l){return await y(l)}let s=R(e.formData,Object.fromEntries(r.entries()));if(s.issues)return await v(s.issues);m=s.value}let C=t.clone(),F={id:o,url:f,req:C,...u?{segments:u}:{},...i?{searchParams:i}:{},...c?{body:c}:{},...m?{formData:m}:{}},h=await A(F);if(h instanceof Response)return n.error(`\u{1F6D1} Request not authorized for route handler '${o}'`),h;let I={id:o,url:f,...h?{auth:h}:{},...u?{segments:u}:{},...i?{searchParams:i}:{},...c?{body:c}:{},...m?{formData:m}:{}};try{let a=await p(I,t);return d.stop(),n.info(`\u2705 Route handler '${o}' executed successfully in ${d.get()}`),a}catch(a){return d.stop(),n.error(`\u{1F6D1} Route handler '${o} failed to execute after ${d.get()}'`),await y(a)}}}export{B as createSafeRouteHandler};
2
2
  //# sourceMappingURL=index.js.map