@sugardarius/anzen 2.2.0 â 2.3.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/README.md +55 -4
- package/dist/chunk-A7I43FHB.js +2 -0
- package/dist/chunk-A7I43FHB.js.map +1 -0
- package/dist/chunk-MJPPHUFP.cjs +2 -0
- package/dist/chunk-MJPPHUFP.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +454 -4
- package/dist/index.d.ts +454 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server-components/index.cjs +1 -1
- package/dist/server-components/index.cjs.map +1 -1
- package/dist/server-components/index.d.cts +6 -6
- package/dist/server-components/index.d.ts +6 -6
- package/dist/server-components/index.js +1 -1
- package/dist/server-components/index.js.map +1 -1
- package/dist/standard-schema-BHEkWkzf.d.cts +73 -0
- package/dist/standard-schema-BHEkWkzf.d.ts +73 -0
- package/package.json +14 -14
- package/dist/chunk-O6UW3BSE.cjs +0 -2
- package/dist/chunk-O6UW3BSE.cjs.map +0 -1
- package/dist/chunk-Y6TQQ4BK.js +0 -2
- package/dist/chunk-Y6TQQ4BK.js.map +0 -1
- package/dist/types-LPIIICMI.d.cts +0 -266
- package/dist/types-LPIIICMI.d.ts +0 -266
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,196 @@
|
|
|
1
|
-
import { A as AuthContext,
|
|
2
|
-
|
|
1
|
+
import { A as AuthContext, S as StandardSchemaDictionary, a as StandardSchemaV1, b as Awaitable, U as UnwrapReadonlyObject, E as EmptyObjectType } from './standard-schema-BHEkWkzf.js';
|
|
2
|
+
|
|
3
|
+
type TSegmentsDict = StandardSchemaDictionary;
|
|
4
|
+
type TSearchParamsDict = StandardSchemaDictionary;
|
|
5
|
+
type TBodySchema = StandardSchemaV1;
|
|
6
|
+
type TFormDataDict = StandardSchemaDictionary;
|
|
7
|
+
type RouteHandlerAuthFunctionParams<TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
|
|
8
|
+
/**
|
|
9
|
+
* ID for the route handler.
|
|
10
|
+
*/
|
|
11
|
+
readonly id: string;
|
|
12
|
+
/**
|
|
13
|
+
* Parsed request url
|
|
14
|
+
*/
|
|
15
|
+
readonly url: URL;
|
|
16
|
+
/**
|
|
17
|
+
* Original request
|
|
18
|
+
*
|
|
19
|
+
* Cloned from the incoming request to avoid side effects
|
|
20
|
+
* and to make it consumable in the `authorize` function.
|
|
21
|
+
* Due to `NextRequest` limitations as the req is cloned it's always a `Request`
|
|
22
|
+
*/
|
|
23
|
+
req: Request;
|
|
24
|
+
} & (TSegments extends TSegmentsDict ? {
|
|
25
|
+
/**
|
|
26
|
+
* Validated route dynamic segments
|
|
27
|
+
*/
|
|
28
|
+
readonly segments: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSegments>>;
|
|
29
|
+
} : EmptyObjectType) & (TSearchParams extends TSearchParamsDict ? {
|
|
30
|
+
/**
|
|
31
|
+
* Validated search params
|
|
32
|
+
*/
|
|
33
|
+
readonly searchParams: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSearchParams>>;
|
|
34
|
+
} : EmptyObjectType) & (TBody extends TBodySchema ? {
|
|
35
|
+
/**
|
|
36
|
+
* Validated request body
|
|
37
|
+
*/
|
|
38
|
+
readonly body: StandardSchemaV1.InferOutput<TBody>;
|
|
39
|
+
} : EmptyObjectType) & (TFormData extends TFormDataDict ? {
|
|
40
|
+
/**
|
|
41
|
+
* Validated form data
|
|
42
|
+
*/
|
|
43
|
+
readonly formData: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TFormData>>;
|
|
44
|
+
} : EmptyObjectType);
|
|
45
|
+
type RouteHandlerAuthFunction<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = (params: RouteHandlerAuthFunctionParams<TSegments, TSearchParams, TBody, TFormData>) => Awaitable<AC | Response | never>;
|
|
46
|
+
type OnValidationErrorResponse = (issues: readonly StandardSchemaV1.Issue[]) => Awaitable<Response>;
|
|
47
|
+
type CreateSafeRouteHandlerOptions<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
|
|
48
|
+
/**
|
|
49
|
+
* ID for the route handler.
|
|
50
|
+
* Used when logging in development or when `debug` is enabled.
|
|
51
|
+
*
|
|
52
|
+
* You can also use it to add extra logging or monitoring.
|
|
53
|
+
*/
|
|
54
|
+
id?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Callback triggered when the request fails.
|
|
57
|
+
* By default it returns a simple `500` response and the error is logged into the console.
|
|
58
|
+
*
|
|
59
|
+
* Use it if your handler use custom errors and
|
|
60
|
+
* you want to manage them properly by returning a proper response.
|
|
61
|
+
*/
|
|
62
|
+
onErrorResponse?: (err: unknown) => Awaitable<Response>;
|
|
63
|
+
/**
|
|
64
|
+
* Use this options to enable debug mode.
|
|
65
|
+
* It will add logs in the handler to help you debug the request.
|
|
66
|
+
*
|
|
67
|
+
* By default it's set to `false` for production builds.
|
|
68
|
+
* In development builds, it will be `true` if `NODE_ENV` is not set to `production`.
|
|
69
|
+
*/
|
|
70
|
+
debug?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Dynamic route segments used for the route handler path.
|
|
73
|
+
* By design it will handler if the segments are a `Promise` or not.
|
|
74
|
+
*
|
|
75
|
+
* Please note the expected input is a `StandardSchemaDictionary`.
|
|
76
|
+
*/
|
|
77
|
+
segments?: TSegments;
|
|
78
|
+
/**
|
|
79
|
+
* Callback triggered when dynamic segments validations returned issues.
|
|
80
|
+
* By default it returns a simple `400` response and issues are logged into the console.
|
|
81
|
+
*/
|
|
82
|
+
onSegmentsValidationErrorResponse?: OnValidationErrorResponse;
|
|
83
|
+
/**
|
|
84
|
+
* Search params used in the route.
|
|
85
|
+
*
|
|
86
|
+
* Please note the expected input is a `StandardSchemaDictionary`.
|
|
87
|
+
*/
|
|
88
|
+
searchParams?: TSearchParams;
|
|
89
|
+
/**
|
|
90
|
+
* Callback triggered when search params validations returned issues.
|
|
91
|
+
* By default it returns a simple `400` response and issues are logged into the console.
|
|
92
|
+
*/
|
|
93
|
+
onSearchParamsValidationErrorResponse?: OnValidationErrorResponse;
|
|
94
|
+
/**
|
|
95
|
+
* Request body.
|
|
96
|
+
*
|
|
97
|
+
* Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
|
|
98
|
+
* Returns a `415`response if the request does not explicitly set the `Content-Type` to `application/json`.
|
|
99
|
+
*
|
|
100
|
+
* IMPORTANT: The body is parsed as JSON, so it must be a valid JSON object!
|
|
101
|
+
* IMPORTANT: Body shouldn't be used with `formData` at the same time. They are exclusive.
|
|
102
|
+
* Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
|
|
103
|
+
*/
|
|
104
|
+
body?: TBody;
|
|
105
|
+
/**
|
|
106
|
+
* Callback triggered when body validation returned issues.
|
|
107
|
+
* By default it returns a simple `400` response and issues are logged into the console.
|
|
108
|
+
*/
|
|
109
|
+
onBodyValidationErrorResponse?: OnValidationErrorResponse;
|
|
110
|
+
/**
|
|
111
|
+
* Request form data.
|
|
112
|
+
*
|
|
113
|
+
* Returns a `405` response if the request method is not `POST`, 'PUT' or 'PATCH'.
|
|
114
|
+
* Returns a `415`response if the request does not explicitly set the `Content-Type` to `multipart/form-data`
|
|
115
|
+
* or to `application/x-www-form-urlencoded`.
|
|
116
|
+
*
|
|
117
|
+
* IMPORTANT: formData shouldn't be used with `body` at the same time. They are exclusive.
|
|
118
|
+
* Why making the distinction? `formData` is used as a `StandardSchemaDictionary` whereas `body` is used as a `StandardSchemaV1`.
|
|
119
|
+
*/
|
|
120
|
+
formData?: TFormData;
|
|
121
|
+
/**
|
|
122
|
+
* Callback triggered when form data validation returned issues.
|
|
123
|
+
* By default it returns a simple `400` response and issues are logged into the console.
|
|
124
|
+
*/
|
|
125
|
+
onFormDataValidationErrorResponse?: OnValidationErrorResponse;
|
|
126
|
+
/**
|
|
127
|
+
* Function to use to authorize the request.
|
|
128
|
+
* By default it always authorize the request.
|
|
129
|
+
*
|
|
130
|
+
* When returning a response, it will be used as the response for the request.
|
|
131
|
+
* Return a response when the request is not authorized.
|
|
132
|
+
*/
|
|
133
|
+
authorize?: RouteHandlerAuthFunction<AC, TSegments, TSearchParams, TBody, TFormData>;
|
|
134
|
+
};
|
|
135
|
+
type ProvidedRouteContext = {
|
|
136
|
+
/**
|
|
137
|
+
* Route dynamic segments as params
|
|
138
|
+
*/
|
|
139
|
+
params: Awaitable<any> | undefined;
|
|
140
|
+
};
|
|
141
|
+
type CreateSafeRouteHandlerReturnType<TReq extends Request = Request> = (
|
|
142
|
+
/**
|
|
143
|
+
* Original request
|
|
144
|
+
*/
|
|
145
|
+
req: TReq,
|
|
146
|
+
/**
|
|
147
|
+
* Provided context added by Next.js itself
|
|
148
|
+
*/
|
|
149
|
+
providedContext: ProvidedRouteContext) => Promise<Response | never>;
|
|
150
|
+
type SafeRouteHandlerContext<AC extends AuthContext | undefined, TSegments extends TSegmentsDict | undefined, TSearchParams extends TSearchParamsDict | undefined, TBody extends TBodySchema | undefined, TFormData extends TFormDataDict | undefined> = {
|
|
151
|
+
/**
|
|
152
|
+
* Route handler ID
|
|
153
|
+
*/
|
|
154
|
+
readonly id: string;
|
|
155
|
+
/**
|
|
156
|
+
* Parsed request url
|
|
157
|
+
*/
|
|
158
|
+
readonly url: URL;
|
|
159
|
+
} & (AC extends AuthContext ? {
|
|
160
|
+
/**
|
|
161
|
+
* Auth context
|
|
162
|
+
*/
|
|
163
|
+
readonly auth: AC;
|
|
164
|
+
} : EmptyObjectType) & (TSegments extends TSegmentsDict ? {
|
|
165
|
+
/**
|
|
166
|
+
* Validated route dynamic segments
|
|
167
|
+
*/
|
|
168
|
+
readonly segments: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSegments>>;
|
|
169
|
+
} : EmptyObjectType) & (TSearchParams extends TSearchParamsDict ? {
|
|
170
|
+
/**
|
|
171
|
+
* Validated search params
|
|
172
|
+
*/
|
|
173
|
+
readonly searchParams: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TSearchParams>>;
|
|
174
|
+
} : EmptyObjectType) & (TBody extends TBodySchema ? {
|
|
175
|
+
/**
|
|
176
|
+
* Validated request body
|
|
177
|
+
*/
|
|
178
|
+
readonly body: StandardSchemaV1.InferOutput<TBody>;
|
|
179
|
+
} : EmptyObjectType) & (TFormData extends TFormDataDict ? {
|
|
180
|
+
/**
|
|
181
|
+
* Validated form data
|
|
182
|
+
*/
|
|
183
|
+
readonly formData: UnwrapReadonlyObject<StandardSchemaDictionary.InferOutput<TFormData>>;
|
|
184
|
+
} : EmptyObjectType);
|
|
185
|
+
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> = (
|
|
186
|
+
/**
|
|
187
|
+
* Safe route handler context
|
|
188
|
+
*/
|
|
189
|
+
ctx: SafeRouteHandlerContext<AC, TSegments, TSearchParams, TBody, TFormData>,
|
|
190
|
+
/**
|
|
191
|
+
* Original request
|
|
192
|
+
*/
|
|
193
|
+
req: TReq) => Promise<Response | never>;
|
|
3
194
|
|
|
4
195
|
/**
|
|
5
196
|
* Creates a safe route handler with data validation and error handling
|
|
@@ -13,7 +204,7 @@ export { f as AuthFunction, g as AuthFunctionParams, e as Awaitable, B as BaseOp
|
|
|
13
204
|
* @example
|
|
14
205
|
* ```ts
|
|
15
206
|
* import { string } from 'decoders'
|
|
16
|
-
*import { createSafeRouteHandler } from '@sugardarius/anzen'
|
|
207
|
+
* import { createSafeRouteHandler } from '@sugardarius/anzen'
|
|
17
208
|
* import { auth } from '~/lib/auth'
|
|
18
209
|
*
|
|
19
210
|
* export const GET = createSafeRouteHandler(
|
|
@@ -39,4 +230,263 @@ export { f as AuthFunction, g as AuthFunctionParams, e as Awaitable, B as BaseOp
|
|
|
39
230
|
*/
|
|
40
231
|
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>;
|
|
41
232
|
|
|
42
|
-
|
|
233
|
+
type TInputSchema = StandardSchemaV1;
|
|
234
|
+
type ServerActionErrorContext = Record<string, unknown>;
|
|
235
|
+
/**
|
|
236
|
+
* Generic server error.
|
|
237
|
+
* Triggered when server action handler throws an unexpected error.
|
|
238
|
+
*
|
|
239
|
+
* Context `ctx` is used to store the error context.
|
|
240
|
+
* It can be customized by using the `onError` option when creating the server action.
|
|
241
|
+
*/
|
|
242
|
+
type ServerError = {
|
|
243
|
+
readonly code: 'SERVER_ERROR';
|
|
244
|
+
readonly ctx: ServerActionErrorContext;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Unauthorized error.
|
|
248
|
+
* Triggered when server action is not authorized by the `authorize` function
|
|
249
|
+
* đđģ When `authorize` function throws an error.
|
|
250
|
+
*
|
|
251
|
+
*
|
|
252
|
+
* Context `ctx` is used to store the error context.
|
|
253
|
+
* It can be customized by using the `onError` option when creating the server action.
|
|
254
|
+
*/
|
|
255
|
+
type UnauthorizedError = {
|
|
256
|
+
readonly code: 'UNAUTHORIZED_ERROR';
|
|
257
|
+
readonly ctx: ServerActionErrorContext;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Validation error.
|
|
261
|
+
* Triggered when server action input validation returns issues.
|
|
262
|
+
*
|
|
263
|
+
* Context `ctx` is used to store the error context.
|
|
264
|
+
* It can be customized by using the `onInputValidationError` option when creating the server action.
|
|
265
|
+
*
|
|
266
|
+
* By default this error will return the issues `StandardSchemaV1.Issue[]` in the context when
|
|
267
|
+
* no context customization is provided.
|
|
268
|
+
*/
|
|
269
|
+
type ValidationError = {
|
|
270
|
+
readonly code: 'VALIDATION_ERROR';
|
|
271
|
+
readonly ctx: ServerActionErrorContext;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Tagged error.
|
|
275
|
+
* It represents an error expected to be used in the server action
|
|
276
|
+
* defined by developers themselves.
|
|
277
|
+
*/
|
|
278
|
+
type TaggedError = {
|
|
279
|
+
readonly code: string;
|
|
280
|
+
readonly ctx: ServerActionErrorContext;
|
|
281
|
+
};
|
|
282
|
+
type SafeServerActionError = ValidationError | UnauthorizedError | ServerError | TaggedError;
|
|
283
|
+
type SafeServerActionResultSuccess<TOutput> = {
|
|
284
|
+
readonly success: true;
|
|
285
|
+
readonly output: TOutput;
|
|
286
|
+
readonly error?: never;
|
|
287
|
+
};
|
|
288
|
+
type SafeServerActionResultError<TError> = {
|
|
289
|
+
readonly success: false;
|
|
290
|
+
readonly output?: never;
|
|
291
|
+
readonly error: TError;
|
|
292
|
+
};
|
|
293
|
+
type ServerActionAuthFunctionParams<TInput extends TInputSchema | undefined> = {
|
|
294
|
+
/**
|
|
295
|
+
* Server action ID
|
|
296
|
+
*/
|
|
297
|
+
readonly id: string;
|
|
298
|
+
} & (TInput extends TInputSchema ? {
|
|
299
|
+
/**
|
|
300
|
+
* Validated input
|
|
301
|
+
*/
|
|
302
|
+
readonly input: UnwrapReadonlyObject<StandardSchemaV1.InferOutput<TInput>>;
|
|
303
|
+
} : EmptyObjectType);
|
|
304
|
+
type ServerActionAuthFunction<AC extends AuthContext | undefined, TInput extends TInputSchema | undefined> = (
|
|
305
|
+
/**
|
|
306
|
+
* Auth function parameters
|
|
307
|
+
* Contains the server action ID and the validated input.
|
|
308
|
+
*
|
|
309
|
+
* If the input is not provided, the property do not exists.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* authorize: async ({ id, input }) => {
|
|
314
|
+
* const auth = await getAuth()
|
|
315
|
+
* if (!auth) {
|
|
316
|
+
* throw new UnauthenticatedError()
|
|
317
|
+
* }
|
|
318
|
+
*
|
|
319
|
+
* const hasAccess = await checkAccess({
|
|
320
|
+
* userId: auth.user.id,
|
|
321
|
+
* resourceId: input.resourceId,
|
|
322
|
+
* })
|
|
323
|
+
*
|
|
324
|
+
* if (!hasAccess) {
|
|
325
|
+
* throw new ForbiddenError()
|
|
326
|
+
* }
|
|
327
|
+
*
|
|
328
|
+
* return { user: auth.user }
|
|
329
|
+
* }
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
params: ServerActionAuthFunctionParams<TInput>) => Awaitable<AC | never>;
|
|
333
|
+
type OnError = (err: unknown) => Awaitable<ServerActionErrorContext>;
|
|
334
|
+
type OnInputValidationError = (issues: readonly StandardSchemaV1.Issue[]) => Awaitable<ServerActionErrorContext>;
|
|
335
|
+
type CreateSafeServerActionOptions<TInput extends TInputSchema | undefined, AC extends AuthContext | undefined> = {
|
|
336
|
+
/**
|
|
337
|
+
* ID for the server action.
|
|
338
|
+
* Used when logging in development or when `debug` is enabled.
|
|
339
|
+
*
|
|
340
|
+
* You can also use it to add extra logging or monitoring.
|
|
341
|
+
*/
|
|
342
|
+
id?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Use this options to enable debug mode.
|
|
345
|
+
* It will add logs in the handler to help you debug server action.
|
|
346
|
+
*
|
|
347
|
+
* By default it's set to `false` for production builds.
|
|
348
|
+
* In development builds, it will be `true` if `NODE_ENV` is not set to `production`.
|
|
349
|
+
*/
|
|
350
|
+
debug?: boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Callback triggered when the server action throws an unhandled error.
|
|
353
|
+
* By default it will return an error context object and the error is logged into the console.
|
|
354
|
+
*
|
|
355
|
+
* Use it if you want to manage unexpected errors properly
|
|
356
|
+
* to log or trace and define custom error contexts objects.
|
|
357
|
+
*
|
|
358
|
+
* â ī¸ By design, this callback isn't mean to be used to manage navigation behaviors like using `notFound` or `redirect`,
|
|
359
|
+
* or with `throw` statements as it's best to handle them in the UI. â ī¸
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* // â
Valid use case
|
|
364
|
+
* onError: async (err: unknown) => {
|
|
365
|
+
* log.error(`đ Unexpected error in server action '${id}'`, err)
|
|
366
|
+
* if (err instanceof NotFoundError) {
|
|
367
|
+
* return {
|
|
368
|
+
* message: 'Resource not found',
|
|
369
|
+
* }
|
|
370
|
+
* }
|
|
371
|
+
*
|
|
372
|
+
* return {
|
|
373
|
+
* message: 'An unexpected error occurred',
|
|
374
|
+
* err: JSON.stringify(err),
|
|
375
|
+
* }
|
|
376
|
+
* }
|
|
377
|
+
*
|
|
378
|
+
* // â Invalid use case
|
|
379
|
+
* onError: async (err: unknown) => {
|
|
380
|
+
* throw err
|
|
381
|
+
* // or redirect('/')
|
|
382
|
+
* }
|
|
383
|
+
*/
|
|
384
|
+
onError?: OnError;
|
|
385
|
+
/**
|
|
386
|
+
* Server action input schema used to validate the input
|
|
387
|
+
* when calling the server action.
|
|
388
|
+
*
|
|
389
|
+
* Please note the expected input is a `StandardSchemaV1`.
|
|
390
|
+
*/
|
|
391
|
+
input?: TInput;
|
|
392
|
+
/**
|
|
393
|
+
* Callback triggered when input validation returned issues
|
|
394
|
+
* By default it will return an error context object and the error is logged into the console.
|
|
395
|
+
*
|
|
396
|
+
* Use it if you want to manage input validation errors properly
|
|
397
|
+
* to log or trace and define custom error contexts objects.
|
|
398
|
+
*
|
|
399
|
+
* â ī¸ By design, this callback isn't mean to be used to manage navigation behaviors like using `notFound` or `redirect`,
|
|
400
|
+
* or with `throw` statements as it's best to handle them in the UI. â ī¸
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* // â
Valid use case
|
|
405
|
+
* onInputValidationError: async (issues: readonly StandardSchemaV1.Issue[]) => {
|
|
406
|
+
* log.error(`đ Invalid input for server action '${id}'`, issues)
|
|
407
|
+
* return {
|
|
408
|
+
* message: 'Invalid input',
|
|
409
|
+
* issues,
|
|
410
|
+
* }
|
|
411
|
+
* }
|
|
412
|
+
*
|
|
413
|
+
* // â Invalid use case
|
|
414
|
+
* onInputValidationError: async (issues: readonly StandardSchemaV1.Issue[]) => {
|
|
415
|
+
* throw new Error('Invalid input')
|
|
416
|
+
* }
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
onInputValidationError?: OnInputValidationError;
|
|
420
|
+
/**
|
|
421
|
+
* Function to use to authorize the server action.
|
|
422
|
+
* By default it always authorize the server action.
|
|
423
|
+
*
|
|
424
|
+
* Returns an unauthorized error when the server action is not authorized
|
|
425
|
+
* or never when `redirect`, `notFound`, `forbidden` or `unauthorized` are thrown.
|
|
426
|
+
*/
|
|
427
|
+
authorize?: ServerActionAuthFunction<AC, TInput>;
|
|
428
|
+
};
|
|
429
|
+
type SafeServerActionResult<TOutput, TError> = SafeServerActionResultSuccess<TOutput> | SafeServerActionResultError<TError> | never;
|
|
430
|
+
type InferServerActionProvidedInput<TInput extends TInputSchema | undefined> = TInput extends TInputSchema ? FormData | StandardSchemaV1.InferOutput<TInput> : undefined;
|
|
431
|
+
type CreateSafeServerActionReturnType<TInput extends TInputSchema | undefined, TOutput, TError> = [TInput] extends [TInputSchema] ? (providedInput: InferServerActionProvidedInput<TInput>) => Promise<SafeServerActionResult<TOutput, TError>> : (providedInput?: InferServerActionProvidedInput<TInput>) => Promise<SafeServerActionResult<TOutput, TError>>;
|
|
432
|
+
type SafeServerActionContext<TInput extends TInputSchema | undefined, AC extends AuthContext | undefined> = {
|
|
433
|
+
/**
|
|
434
|
+
* Server action ID
|
|
435
|
+
*/
|
|
436
|
+
readonly id: string;
|
|
437
|
+
/**
|
|
438
|
+
* Tag error function
|
|
439
|
+
* Throws a developer defined tagged error.
|
|
440
|
+
* @example
|
|
441
|
+
* ```ts
|
|
442
|
+
* // Server
|
|
443
|
+
* export const myAction = createSafeServerAction({
|
|
444
|
+
* id: 'my action,
|
|
445
|
+
* }, async ({ tagErr }) => {
|
|
446
|
+
* tagErr('CONFLICT', {
|
|
447
|
+
* message: 'resource already exists',
|
|
448
|
+
* })
|
|
449
|
+
* })
|
|
450
|
+
*
|
|
451
|
+
* // Client
|
|
452
|
+
* const result = await myAction()
|
|
453
|
+
* if (result.success === false) {
|
|
454
|
+
* if (result.error.code === 'CONFLICT') {
|
|
455
|
+
* return <span>{result.error.ctx.message}</span>
|
|
456
|
+
* }
|
|
457
|
+
* }
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
readonly tagErr: (code: string, ctx: ServerActionErrorContext) => never;
|
|
461
|
+
} & (AC extends AuthContext ? {
|
|
462
|
+
/**
|
|
463
|
+
* Auth context
|
|
464
|
+
*/
|
|
465
|
+
readonly auth: AC;
|
|
466
|
+
} : EmptyObjectType) & (TInput extends TInputSchema ? {
|
|
467
|
+
/**
|
|
468
|
+
* Validated input
|
|
469
|
+
*/
|
|
470
|
+
readonly input: UnwrapReadonlyObject<StandardSchemaV1.InferOutput<TInput>>;
|
|
471
|
+
} : EmptyObjectType);
|
|
472
|
+
type SafeServerActionHandler<TOutput, TInput extends TInputSchema | undefined, AC extends AuthContext | undefined> = (
|
|
473
|
+
/**
|
|
474
|
+
* Safe server action context
|
|
475
|
+
*/
|
|
476
|
+
ctx: SafeServerActionContext<TInput, AC>) => Promise<TOutput | never>;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Overload for server actions with no input.
|
|
480
|
+
* Used when calling server actions with no input schema provided,
|
|
481
|
+
* making the DX for developers easier and nicer. It avoids to call
|
|
482
|
+
* a server action with `undefined` as input.
|
|
483
|
+
*/
|
|
484
|
+
declare function createSafeServerAction<TOutput, AC extends AuthContext | undefined = undefined>(options: CreateSafeServerActionOptions<undefined, AC>, handler: SafeServerActionHandler<TOutput, undefined, AC>): CreateSafeServerActionReturnType<undefined, TOutput, SafeServerActionError>;
|
|
485
|
+
/**
|
|
486
|
+
* Overload for server actions with input.
|
|
487
|
+
*/
|
|
488
|
+
declare function createSafeServerAction<TOutput, TInput extends TInputSchema, AC extends AuthContext | undefined = undefined>(options: CreateSafeServerActionOptions<TInput, AC> & {
|
|
489
|
+
input: TInput;
|
|
490
|
+
}, handler: SafeServerActionHandler<TOutput, TInput, AC>): CreateSafeServerActionReturnType<TInput, TOutput, SafeServerActionError>;
|
|
491
|
+
|
|
492
|
+
export { AuthContext, Awaitable, type CreateSafeRouteHandlerOptions, type CreateSafeRouteHandlerReturnType, type CreateSafeServerActionOptions, type CreateSafeServerActionReturnType, type InferServerActionProvidedInput, type OnValidationErrorResponse, type ProvidedRouteContext, type RouteHandlerAuthFunction, type RouteHandlerAuthFunctionParams, type SafeRouteHandler, type SafeRouteHandlerContext, type SafeServerActionContext, type SafeServerActionError, type SafeServerActionHandler, type SafeServerActionResult, type SafeServerActionResultError, type SafeServerActionResultSuccess, type ServerActionAuthFunction, type ServerActionAuthFunctionParams, type ServerActionErrorContext, type ServerError, type TBodySchema, type TFormDataDict, type TSearchParamsDict, type TSegmentsDict, type UnauthorizedError, type ValidationError, createSafeRouteHandler, createSafeServerAction };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as
|
|
1
|
+
import{a as C,b as g,c as h,d as $,e as E,f as I,g as D}from"./chunk-A7I43FHB.js";var H="[unknown:route:handler]",F=async r=>{if(r.headers.get("content-type")?.startsWith("application/json"))return await r.json();let t=await r.text();return JSON.parse(t)};function V(r,S){if(r.body&&r.formData)throw new Error("You cannot use both `body` and `formData` in the same route handler. They are both mutually exclusive.");let t=C(r.debug),a=r.id??H,m=r.onErrorResponse??(e=>(t.error(`\u{1F6D1} Unexpected error in route handler '${a}'`,e),new Response("Internal server error",{status:500}))),y=r.onSegmentsValidationErrorResponse??(e=>(t.error(`\u{1F6D1} Invalid segments for route handler '${a}':`,e),new Response("Invalid segments",{status:400}))),T=r.onSearchParamsValidationErrorResponse??(e=>(t.error(`\u{1F6D1} Invalid search params for route handler '${a}':`,e),new Response("Invalid search params",{status:400}))),x=r.onBodyValidationErrorResponse??(e=>(t.error(`\u{1F6D1} Invalid body for route handler '${a}':`,e),new Response("Invalid body",{status:400}))),w=r.onFormDataValidationErrorResponse??(e=>(t.error(`\u{1F6D1} Invalid form data for route handler '${a}':`,e),new Response("Invalid form data",{status:400}))),P=r.authorize??(async()=>{});return async function(e,c){let s=g();s.start(),t.info(`\u{1F504} Running route handler '${a}'`),t.info(`\u{1F449}\u{1F3FB} Request ${e.method} ${e.url}`);let f=new URL(e.url),l;if(r.segments){let o=await c.params;if(o===void 0)return new Response("No segments provided",{status:400});let i=D(r.segments,o);if(i.issues)return await y(i.issues);l=i.value}let n;if(r.searchParams){let o=[...f.searchParams.keys()].map(u=>{let v=f.searchParams.getAll(u);return[u,v.length>1?v:v[0]]}),i=D(r.searchParams,Object.fromEntries(o));if(i.issues)return await T(i.issues);n=i.value}let d=e.clone(),p;if(r.body){if(!["POST","PUT","PATCH"].includes(e.method))return new Response("Invalid method for request body",{status:405});let o;try{o=await F(d)}catch(u){return await m(u)}let i=I(r.body,o,"Request body validation must be synchronous");if(i.issues)return await x(i.issues);p=i.value}let R;if(r.formData){if(!["POST","PUT","PATCH"].includes(e.method))return new Response("Invalid method for request form data",{status:405});let o=e.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 i;try{i=await d.formData()}catch(v){return await m(v)}let u=D(r.formData,Object.fromEntries(i.entries()));if(u.issues)return await w(u.issues);R=u.value}let b;try{let o=e.clone(),i={id:a,url:f,req:o,...l?{segments:l}:{},...n?{searchParams:n}:{},...p?{body:p}:{},...R?{formData:R}:{}},u=await P(i);if(u instanceof Response)return s.stop(),t.error(`\u{1F6D1} Request not authorized for route handler '${a}' after ${s.get()}`),u;b=u}catch(o){if(s.stop(),t.error(`\u{1F6D1} Request not authorized for route handler '${a}' after ${s.get()}`),h(o))throw o;return await m(o)}let O={id:a,url:f,...b?{auth:b}:{},...l?{segments:l}:{},...n?{searchParams:n}:{},...p?{body:p}:{},...R?{formData:R}:{}};try{let o=await S(O,e);return s.stop(),t.info(`\u2705 Route handler '${a}' executed successfully in ${s.get()}`),o}catch(o){if(s.stop(),h(o))throw t.info("\u2139\uFE0F Ignoring native Next.js error"),t.info(`\u2705 Route handler '${a}' executed successfully in ${s.get()}`),o;return t.error(`\u{1F6D1} Route handler '${a} failed to execute after ${s.get()}'`),await m(o)}}}var A=class extends Error{code;ctx;constructor(S,t){super("developer defined tagged error"),this.code=S,this.ctx=t,this.name="KTaggedError"}};var k="[unknown:server:action]";function N(r,S){let t=C(r.debug),a=r.id??k,m=r.authorize??(async()=>{}),y=e=>(t.error(`\u{1F6D1} Unexpected error in server action '${a}'`,e),$(e)?{message:e.message,name:e.name}:{message:JSON.stringify(e)}),T=e=>(t.error(`\u{1F534} 'onError' callback in server action '${a}' threw an error. Falling back to build-in error context.`),y(e)),x=r.onError??y,w=e=>(t.error(`\u{1F6D1} Invalid input for server action '${a}'`,e),{issues:e}),P=r.onInputValidationError??w;return async function(e){let c=g();c.start(),t.info(`\u{1F504} Running server action '${a}'`);let s;if(r.input){let n;e instanceof FormData?n=Object.fromEntries(e.entries()):n=e;let d=I(r.input,n);if(d.issues)return{success:!1,error:{code:"VALIDATION_ERROR",ctx:await E(()=>P(d.issues),()=>(t.error(`\u{1F534} 'onInputValidationError' callback in server action '${a}' threw an error while validating input. Falling back to build-in input validation error context.`),w(d.issues)))}};s=d.value}let f;try{let n={id:a,...s?{input:s}:{}};f=await m(n)}catch(n){if(c.stop(),t.error(`\u{1F534} Server action '${a}' not authorized after ${c.get()}`),h(n))throw n;return{success:!1,error:{code:"UNAUTHORIZED_ERROR",ctx:await E(()=>x(n),()=>T(n))}}}let l={id:a,tagErr:(n,d)=>{throw new A(n,d)},...f?{auth:f}:{},...s?{input:s}:{}};try{let n=await S(l);return c.stop(),t.info(`\u2705 Server action '${a}' executed successfully in ${c.get()}`),{success:!0,output:n}}catch(n){if(c.stop(),h(n))throw t.info(`\u2139\uFE0F Ignoring native Next.js error in server action '${a}'`),t.info(`\u2705 Server action '${a}' executed successfully in ${c.get()}`),n;return n instanceof A?(t.info(`\u2705 Server action '${a}' executed successfully in ${c.get()}`),{success:!1,error:{code:n.code,ctx:n.ctx}}):(t.error(`\u{1F534} Server action '${a}' failed to execute after ${c.get()}`),{success:!1,error:{code:"SERVER_ERROR",ctx:await E(()=>x(n),()=>T(n))}})}}}export{V as createSafeRouteHandler,N as createSafeServerAction};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/create-safe-route-handler.ts"],"sourcesContent":["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"],"mappings":"6DAsBO,IAAMA,EAAa,0BAQpBC,EAAwB,MAC5BC,GACqB,CAErB,GADoBA,EAAI,QAAQ,IAAI,cAAc,GACjC,WAAW,kBAAkB,EAC5C,OAAO,MAAMA,EAAI,KAAK,EAGxB,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAC5B,OAAO,KAAK,MAAMC,CAAI,CACxB,EAsCO,SAASC,EAQdC,EAOAC,EAQwC,CAExC,GAAID,EAAQ,MAAQA,EAAQ,SAC1B,MAAM,IAAI,MACR,wGACF,EAGF,IAAME,EAAMC,EAAaH,EAAQ,KAAK,EAChCI,EAAKJ,EAAQ,IAAML,EAEnBU,EACJL,EAAQ,kBACNM,IACAJ,EAAI,MAAM,gDAAyCE,CAAE,IAAKE,CAAG,EACtD,IAAI,SAAS,wBAAyB,CAC3C,OAAQ,GACV,CAAC,IAGCC,EACJP,EAAQ,oCACNQ,IACAN,EAAI,MAAM,iDAA0CE,CAAE,KAAMI,CAAM,EAC3D,IAAI,SAAS,mBAAoB,CACtC,OAAQ,GACV,CAAC,IAGCC,EACJT,EAAQ,wCACNQ,IACAN,EAAI,MAAM,sDAA+CE,CAAE,KAAMI,CAAM,EAChE,IAAI,SAAS,wBAAyB,CAC3C,OAAQ,GACV,CAAC,IAGCE,EACJV,EAAQ,gCACNQ,IACAN,EAAI,MAAM,6CAAsCE,CAAE,KAAMI,CAAM,EACvD,IAAI,SAAS,eAAgB,CAClC,OAAQ,GACV,CAAC,IAGCG,EACJX,EAAQ,oCACNQ,IACAN,EAAI,MAAM,kDAA2CE,CAAE,KAAMI,CAAM,EAC5D,IAAI,SAAS,oBAAqB,CACvC,OAAQ,GACV,CAAC,IAGCI,EAAYZ,EAAQ,YAAc,SAAS,IAGjD,OAAO,eACLH,EACAgB,EACmB,CACnB,IAAMC,EAAiBC,EAAqB,EAC5CD,EAAe,MAAM,EAErBZ,EAAI,KAAK,oCAA6BE,CAAE,GAAG,EAC3CF,EAAI,KAAK,8BAAgBL,EAAI,MAAM,IAAIA,EAAI,GAAG,EAAE,EAEhD,IAAMmB,EAAM,IAAI,IAAInB,EAAI,GAAG,EAEvBoB,EACJ,GAAIjB,EAAQ,SAAU,CACpB,IAAMkB,EAAS,MAAML,EAAgB,OACrC,GAAIK,IAAW,OACb,OAAO,IAAI,SAAS,uBAAwB,CAAE,OAAQ,GAAI,CAAC,EAG7D,IAAMC,EAAiBC,EAAoBpB,EAAQ,SAAUkB,CAAM,EACnE,GAAIC,EAAe,OACjB,OAAO,MAAMZ,EAAkCY,EAAe,MAAM,EAGtEF,EAAWE,EAAe,KAC5B,CAEA,IAAIE,EACJ,GAAIrB,EAAQ,aAAc,CACxB,IAAMsB,EAAqB,CAAC,GAAGN,EAAI,aAAa,KAAK,CAAC,EAAE,IAAKO,GAAM,CACjE,IAAMC,EAASR,EAAI,aAAa,OAAOO,CAAC,EACxC,MAAO,CAACA,EAAGC,EAAO,OAAS,EAAIA,EAASA,EAAO,CAAC,CAAC,CACnD,CAAC,EAEKC,EAAqBL,EACzBpB,EAAQ,aACR,OAAO,YAAYsB,CAAkB,CACvC,EAEA,GAAIG,EAAmB,OACrB,OAAO,MAAMhB,EACXgB,EAAmB,MACrB,EAGFJ,EAAeI,EAAmB,KACpC,CAGA,IAAMC,EAAoB7B,EAAI,MAAM,EAEhC8B,EACJ,GAAI3B,EAAQ,KAAM,CAChB,GAAI,CAAC,CAAC,OAAQ,MAAO,OAAO,EAAE,SAASH,EAAI,MAAM,EAC/C,OAAO,IAAI,SAAS,kCAAmC,CACrD,OAAQ,GACV,CAAC,EAGH,IAAI+B,EACJ,GAAI,CACFA,EAAc,MAAMhC,EAAsB8B,CAAiB,CAC7D,OAASpB,EAAK,CACZ,OAAO,MAAMD,EAAgBC,CAAG,CAClC,CAEA,IAAMuB,EAAaC,EACjB9B,EAAQ,KACR4B,EACA,6CACF,EAEA,GAAIC,EAAW,OACb,OAAO,MAAMnB,EAA8BmB,EAAW,MAAM,EAG9DF,EAAOE,EAAW,KACpB,CAEA,IAAIE,EACJ,GAAI/B,EAAQ,SAAU,CACpB,GAAI,CAAC,CAAC,OAAQ,MAAO,OAAO,EAAE,SAASH,EAAI,MAAM,EAC/C,OAAO,IAAI,SAAS,uCAAwC,CAC1D,OAAQ,GACV,CAAC,EAGH,IAAMmC,EAAcnC,EAAI,QAAQ,IAAI,cAAc,EAClD,GACE,CAACmC,GAAa,WAAW,qBAAqB,GAC9C,CAACA,GAAa,WAAW,mCAAmC,EAE5D,OAAO,IAAI,SAAS,6CAA8C,CAChE,OAAQ,GACV,CAAC,EAGH,IAAIC,EACJ,GAAI,CAEFA,EAAkB,MAAMP,EAAkB,SAAS,CACrD,OAASpB,EAAK,CACZ,OAAO,MAAMD,EAAgBC,CAAG,CAClC,CAEA,IAAM4B,EAAiBd,EACrBpB,EAAQ,SACR,OAAO,YAAYiC,EAAgB,QAAQ,CAAC,CAC9C,EAEA,GAAIC,EAAe,OACjB,OAAO,MAAMvB,EAAkCuB,EAAe,MAAM,EAGtEH,EAAWG,EAAe,KAC5B,CAIA,IAAMC,EAAoBtC,EAAI,MAAM,EAC9BuC,EAAa,CACjB,GAAAhC,EACA,IAAAY,EACA,IAAKmB,EACL,GAAIlB,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,EAC/B,GAAII,EAAe,CAAE,aAAAA,CAAa,EAAI,CAAC,EACvC,GAAIM,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAII,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,CACjC,EAMMM,EAAiB,MAAMzB,EAAUwB,CAAU,EACjD,GAAIC,aAA0B,SAC5B,OAAAnC,EAAI,MAAM,uDAAgDE,CAAE,GAAG,EACxDiC,EAIT,IAAMC,EAAM,CACV,GAAAlC,EACA,IAAAY,EACA,GAAIqB,EAAiB,CAAE,KAAMA,CAAe,EAAI,CAAC,EACjD,GAAIpB,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,EAC/B,GAAII,EAAe,CAAE,aAAAA,CAAa,EAAI,CAAC,EACvC,GAAIM,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAII,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,CACjC,EASA,GAAI,CACF,IAAMQ,EAAW,MAAMtC,EAAUqC,EAAKzC,CAAG,EAEzC,OAAAiB,EAAe,KAAK,EACpBZ,EAAI,KACF,yBAAoBE,CAAE,8BAA8BU,EAAe,IAAI,CAAC,EAC1E,EAEOyB,CACT,OAASjC,EAAK,CACZ,OAAAQ,EAAe,KAAK,EACpBZ,EAAI,MACF,4BAAqBE,CAAE,4BAA4BU,EAAe,IAAI,CAAC,GACzE,EAEO,MAAMT,EAAgBC,CAAG,CAClC,CACF,CACF","names":["DEFAULT_ID","readRequestBodyAsJson","req","text","createSafeRouteHandler","options","handlerFn","log","createLogger","id","onErrorResponse","err","onSegmentsValidationErrorResponse","issues","onSearchParamsValidationErrorResponse","onBodyValidationErrorResponse","onFormDataValidationErrorResponse","authorize","providedContext","executionClock","createExecutionClock","url","segments","params","parsedSegments","parseWithDictionary","searchParams","queryParams_unsafe","k","values","parsedSearchParams","clonedReq_forBody","body","body_unsafe","parsedBody","validateWithSchema","formData","contentType","formData_unsafe","parsedFormData","clonedReq_forAuth","authParams","authOrResponse","ctx","response"]}
|
|
1
|
+
{"version":3,"sources":["../src/route-handler/create-safe-route-handler.ts","../src/server-action/errors.ts","../src/server-action/create-safe-server-action.ts"],"sourcesContent":["import { createLogger, createExecutionClock, isNextNativeError } from '../utils'\nimport {\n parseWithDictionary,\n validateWithSchema,\n type StandardSchemaV1,\n} from '../standard-schema'\nimport type { Awaitable, AuthContext } from '../types'\nimport type {\n TSegmentsDict,\n TSearchParamsDict,\n TBodySchema,\n TFormDataDict,\n ProvidedRouteContext,\n CreateSafeRouteHandlerOptions,\n CreateSafeRouteHandlerReturnType,\n SafeRouteHandler,\n SafeRouteHandlerContext,\n RouteHandlerAuthFunctionParams,\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 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 // NOTE: 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 let auth = undefined\n try {\n // NOTE: 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 RouteHandlerAuthFunctionParams<\n TRouteDynamicSegments,\n TSearchParams,\n TBody,\n TFormData\n >\n const authOrResponse = await authorize(authParams)\n if (authOrResponse instanceof Response) {\n executionClock.stop()\n log.error(\n `đ Request not authorized for route handler '${id}' after ${executionClock.get()}`\n )\n return authOrResponse\n }\n\n auth = authOrResponse\n } catch (err: unknown) {\n executionClock.stop()\n\n log.error(\n `đ Request not authorized for route handler '${id}' after ${executionClock.get()}`\n )\n\n if (isNextNativeError(err)) {\n throw err\n }\n return await onErrorResponse(err)\n }\n\n const ctx = {\n id,\n url,\n ...(auth ? { auth: auth } : {}),\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 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\n if (isNextNativeError(err)) {\n log.info('âšī¸ Ignoring native Next.js error')\n log.info(\n `â
Route handler '${id}' executed successfully in ${executionClock.get()}`\n )\n\n throw err\n }\n\n log.error(\n `đ Route handler '${id} failed to execute after ${executionClock.get()}'`\n )\n return await onErrorResponse(err)\n }\n }\n}\n","import type { ServerActionErrorContext } from './types'\n\n/**\n * Developer defined tagged error.\n *\n * It represents an error expected to be used in the server action\n * defined by developers themselves.\n */\nexport class KTaggedError extends Error {\n readonly code: string\n readonly ctx: ServerActionErrorContext\n\n constructor(code: string, ctx: ServerActionErrorContext) {\n super('developer defined tagged error')\n\n this.code = code\n this.ctx = ctx\n\n this.name = 'KTaggedError'\n }\n}\n","import { validateWithSchema, type StandardSchemaV1 } from '../standard-schema'\nimport type { AuthContext, Awaitable } from '../types'\nimport {\n assertsNoThrow,\n createExecutionClock,\n createLogger,\n isNativeError,\n isNextNativeError,\n} from '../utils'\nimport { KTaggedError } from './errors'\nimport type {\n ServerActionAuthFunctionParams,\n CreateSafeServerActionOptions,\n CreateSafeServerActionReturnType,\n InferServerActionProvidedInput,\n SafeServerActionContext,\n SafeServerActionError,\n SafeServerActionHandler,\n SafeServerActionResult,\n ServerActionErrorContext,\n TInputSchema,\n} from './types'\n\n/** @internal exported for testing only */\nexport const DEFAULT_ACTION_ID = '[unknown:server:action]'\n\n/**\n * Overload for server actions with no input.\n * Used when calling server actions with no input schema provided,\n * making the DX for developers easier and nicer. It avoids to call\n * a server action with `undefined` as input.\n */\nexport function createSafeServerAction<\n TOutput,\n AC extends AuthContext | undefined = undefined,\n>(\n options: CreateSafeServerActionOptions<undefined, AC>,\n handler: SafeServerActionHandler<TOutput, undefined, AC>\n): CreateSafeServerActionReturnType<undefined, TOutput, SafeServerActionError>\n\n/**\n * Overload for server actions with input.\n */\nexport function createSafeServerAction<\n TOutput,\n TInput extends TInputSchema,\n AC extends AuthContext | undefined = undefined,\n>(\n options: CreateSafeServerActionOptions<TInput, AC> & { input: TInput },\n handler: SafeServerActionHandler<TOutput, TInput, AC>\n): CreateSafeServerActionReturnType<TInput, TOutput, SafeServerActionError>\n\n/**\n * Creates a safe server action with input validation and error handling\n * for Next.js (>= 14) server actions.\n *\n * @param options - Options to configure the server action.\n * @param actionFn - The server action function.\n *\n * @returns A Next.js server action function.\n */\nexport function createSafeServerAction<\n TOutput,\n TInput extends TInputSchema | undefined = undefined,\n AC extends AuthContext | undefined = undefined,\n>(\n options: CreateSafeServerActionOptions<TInput, AC>,\n handler: SafeServerActionHandler<TOutput, TInput, AC>\n): CreateSafeServerActionReturnType<TInput, TOutput, SafeServerActionError> {\n const log = createLogger(options.debug)\n const id = options.id ?? DEFAULT_ACTION_ID\n\n const authorize = options.authorize ?? (async () => undefined)\n\n const onError_fallback = (\n err: unknown\n ): Awaitable<ServerActionErrorContext> => {\n log.error(`đ Unexpected error in server action '${id}'`, err)\n\n if (isNativeError(err)) {\n return {\n message: err.message,\n name: err.name,\n }\n }\n\n return {\n message: JSON.stringify(err),\n }\n }\n\n const onError_fallbackNoThrow = (err: unknown) => {\n log.error(\n `đ´ 'onError' callback in server action '${id}' threw an error. Falling back to build-in error context.`\n )\n return onError_fallback(err)\n }\n\n const onError = options.onError ?? onError_fallback\n\n const onInputValidationError_fallback = (\n issues: readonly StandardSchemaV1.Issue[]\n ): Awaitable<ServerActionErrorContext> => {\n log.error(`đ Invalid input for server action '${id}'`, issues)\n return {\n issues,\n }\n }\n\n const onInputValidationError =\n options.onInputValidationError ?? onInputValidationError_fallback\n\n return async function (\n providedInput?: InferServerActionProvidedInput<TInput>\n ): Promise<SafeServerActionResult<TOutput, SafeServerActionError>> {\n const executionClock = createExecutionClock()\n executionClock.start()\n\n log.info(`đ Running server action '${id}'`)\n\n let input = undefined\n if (options.input) {\n let input_unsafe: unknown = undefined\n if (providedInput instanceof FormData) {\n input_unsafe = Object.fromEntries(providedInput.entries())\n } else {\n input_unsafe = providedInput\n }\n\n const parsedInput = validateWithSchema(options.input, input_unsafe)\n if (parsedInput.issues) {\n const ctx = await assertsNoThrow(\n () => onInputValidationError(parsedInput.issues),\n () => {\n log.error(\n `đ´ 'onInputValidationError' callback in server action '${id}' threw an error while validating input. Falling back to build-in input validation error context.`\n )\n\n return onInputValidationError_fallback(parsedInput.issues)\n }\n )\n\n return {\n success: false,\n error: {\n code: 'VALIDATION_ERROR',\n ctx,\n },\n }\n }\n\n input = parsedInput.value\n }\n\n let auth = undefined\n try {\n const authParams = {\n id,\n ...(input ? { input } : {}),\n } as ServerActionAuthFunctionParams<TInput>\n\n auth = await authorize(authParams)\n } catch (err: unknown) {\n executionClock.stop()\n\n log.error(\n `đ´ Server action '${id}' not authorized after ${executionClock.get()}`\n )\n\n if (isNextNativeError(err)) {\n throw err\n }\n\n const ctx = await assertsNoThrow(\n () => onError(err),\n () => onError_fallbackNoThrow(err)\n )\n\n return {\n success: false,\n error: {\n code: 'UNAUTHORIZED_ERROR',\n ctx,\n },\n }\n }\n\n const ctx = {\n id,\n tagErr: (code: string, ctx: ServerActionErrorContext): never => {\n throw new KTaggedError(code, ctx)\n },\n ...(auth ? { auth } : {}),\n ...(input ? { input } : {}),\n } as SafeServerActionContext<TInput, AC>\n\n try {\n const output = await handler(ctx)\n\n executionClock.stop()\n log.info(\n `â
Server action '${id}' executed successfully in ${executionClock.get()}`\n )\n\n return {\n success: true,\n output,\n }\n } catch (err: unknown) {\n executionClock.stop()\n\n if (isNextNativeError(err)) {\n log.info(`âšī¸ Ignoring native Next.js error in server action '${id}'`)\n log.info(\n `â
Server action '${id}' executed successfully in ${executionClock.get()}`\n )\n throw err\n }\n\n if (err instanceof KTaggedError) {\n log.info(\n `â
Server action '${id}' executed successfully in ${executionClock.get()}`\n )\n return {\n success: false,\n error: {\n code: err.code,\n ctx: err.ctx,\n },\n }\n }\n\n log.error(\n `đ´ Server action '${id}' failed to execute after ${executionClock.get()}`\n )\n\n const ctx = await assertsNoThrow(\n () => onError(err),\n () => onError_fallbackNoThrow(err)\n )\n\n return {\n success: false,\n error: {\n code: 'SERVER_ERROR',\n ctx,\n },\n }\n }\n }\n}\n"],"mappings":"kFAqBO,IAAMA,EAAa,0BAQpBC,EAAwB,MAC5BC,GACqB,CAErB,GADoBA,EAAI,QAAQ,IAAI,cAAc,GACjC,WAAW,kBAAkB,EAC5C,OAAO,MAAMA,EAAI,KAAK,EAGxB,IAAMC,EAAO,MAAMD,EAAI,KAAK,EAC5B,OAAO,KAAK,MAAMC,CAAI,CACxB,EAsCO,SAASC,EAQdC,EAOAC,EAQwC,CAExC,GAAID,EAAQ,MAAQA,EAAQ,SAC1B,MAAM,IAAI,MACR,wGACF,EAGF,IAAME,EAAMC,EAAaH,EAAQ,KAAK,EAChCI,EAAKJ,EAAQ,IAAML,EAEnBU,EACJL,EAAQ,kBACNM,IACAJ,EAAI,MAAM,gDAAyCE,CAAE,IAAKE,CAAG,EACtD,IAAI,SAAS,wBAAyB,CAC3C,OAAQ,GACV,CAAC,IAGCC,EACJP,EAAQ,oCACNQ,IACAN,EAAI,MAAM,iDAA0CE,CAAE,KAAMI,CAAM,EAC3D,IAAI,SAAS,mBAAoB,CACtC,OAAQ,GACV,CAAC,IAGCC,EACJT,EAAQ,wCACNQ,IACAN,EAAI,MAAM,sDAA+CE,CAAE,KAAMI,CAAM,EAChE,IAAI,SAAS,wBAAyB,CAC3C,OAAQ,GACV,CAAC,IAGCE,EACJV,EAAQ,gCACNQ,IACAN,EAAI,MAAM,6CAAsCE,CAAE,KAAMI,CAAM,EACvD,IAAI,SAAS,eAAgB,CAClC,OAAQ,GACV,CAAC,IAGCG,EACJX,EAAQ,oCACNQ,IACAN,EAAI,MAAM,kDAA2CE,CAAE,KAAMI,CAAM,EAC5D,IAAI,SAAS,oBAAqB,CACvC,OAAQ,GACV,CAAC,IAGCI,EAAYZ,EAAQ,YAAc,SAAS,IAEjD,OAAO,eACLH,EACAgB,EACmB,CACnB,IAAMC,EAAiBC,EAAqB,EAC5CD,EAAe,MAAM,EAErBZ,EAAI,KAAK,oCAA6BE,CAAE,GAAG,EAC3CF,EAAI,KAAK,8BAAgBL,EAAI,MAAM,IAAIA,EAAI,GAAG,EAAE,EAEhD,IAAMmB,EAAM,IAAI,IAAInB,EAAI,GAAG,EAEvBoB,EACJ,GAAIjB,EAAQ,SAAU,CACpB,IAAMkB,EAAS,MAAML,EAAgB,OACrC,GAAIK,IAAW,OACb,OAAO,IAAI,SAAS,uBAAwB,CAAE,OAAQ,GAAI,CAAC,EAG7D,IAAMC,EAAiBC,EAAoBpB,EAAQ,SAAUkB,CAAM,EACnE,GAAIC,EAAe,OACjB,OAAO,MAAMZ,EAAkCY,EAAe,MAAM,EAGtEF,EAAWE,EAAe,KAC5B,CAEA,IAAIE,EACJ,GAAIrB,EAAQ,aAAc,CACxB,IAAMsB,EAAqB,CAAC,GAAGN,EAAI,aAAa,KAAK,CAAC,EAAE,IAAKO,GAAM,CACjE,IAAMC,EAASR,EAAI,aAAa,OAAOO,CAAC,EACxC,MAAO,CAACA,EAAGC,EAAO,OAAS,EAAIA,EAASA,EAAO,CAAC,CAAC,CACnD,CAAC,EAEKC,EAAqBL,EACzBpB,EAAQ,aACR,OAAO,YAAYsB,CAAkB,CACvC,EAEA,GAAIG,EAAmB,OACrB,OAAO,MAAMhB,EACXgB,EAAmB,MACrB,EAGFJ,EAAeI,EAAmB,KACpC,CAGA,IAAMC,EAAoB7B,EAAI,MAAM,EAEhC8B,EACJ,GAAI3B,EAAQ,KAAM,CAChB,GAAI,CAAC,CAAC,OAAQ,MAAO,OAAO,EAAE,SAASH,EAAI,MAAM,EAC/C,OAAO,IAAI,SAAS,kCAAmC,CACrD,OAAQ,GACV,CAAC,EAGH,IAAI+B,EACJ,GAAI,CACFA,EAAc,MAAMhC,EAAsB8B,CAAiB,CAC7D,OAASpB,EAAK,CACZ,OAAO,MAAMD,EAAgBC,CAAG,CAClC,CAEA,IAAMuB,EAAaC,EACjB9B,EAAQ,KACR4B,EACA,6CACF,EAEA,GAAIC,EAAW,OACb,OAAO,MAAMnB,EAA8BmB,EAAW,MAAM,EAG9DF,EAAOE,EAAW,KACpB,CAEA,IAAIE,EACJ,GAAI/B,EAAQ,SAAU,CACpB,GAAI,CAAC,CAAC,OAAQ,MAAO,OAAO,EAAE,SAASH,EAAI,MAAM,EAC/C,OAAO,IAAI,SAAS,uCAAwC,CAC1D,OAAQ,GACV,CAAC,EAGH,IAAMmC,EAAcnC,EAAI,QAAQ,IAAI,cAAc,EAClD,GACE,CAACmC,GAAa,WAAW,qBAAqB,GAC9C,CAACA,GAAa,WAAW,mCAAmC,EAE5D,OAAO,IAAI,SAAS,6CAA8C,CAChE,OAAQ,GACV,CAAC,EAGH,IAAIC,EACJ,GAAI,CAEFA,EAAkB,MAAMP,EAAkB,SAAS,CACrD,OAASpB,EAAK,CACZ,OAAO,MAAMD,EAAgBC,CAAG,CAClC,CAEA,IAAM4B,EAAiBd,EACrBpB,EAAQ,SACR,OAAO,YAAYiC,EAAgB,QAAQ,CAAC,CAC9C,EAEA,GAAIC,EAAe,OACjB,OAAO,MAAMvB,EAAkCuB,EAAe,MAAM,EAGtEH,EAAWG,EAAe,KAC5B,CAEA,IAAIC,EACJ,GAAI,CAGF,IAAMC,EAAoBvC,EAAI,MAAM,EAC9BwC,EAAa,CACjB,GAAAjC,EACA,IAAAY,EACA,IAAKoB,EACL,GAAInB,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,EAC/B,GAAII,EAAe,CAAE,aAAAA,CAAa,EAAI,CAAC,EACvC,GAAIM,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAII,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,CACjC,EAMMO,EAAiB,MAAM1B,EAAUyB,CAAU,EACjD,GAAIC,aAA0B,SAC5B,OAAAxB,EAAe,KAAK,EACpBZ,EAAI,MACF,uDAAgDE,CAAE,WAAWU,EAAe,IAAI,CAAC,EACnF,EACOwB,EAGTH,EAAOG,CACT,OAAShC,EAAc,CAOrB,GANAQ,EAAe,KAAK,EAEpBZ,EAAI,MACF,uDAAgDE,CAAE,WAAWU,EAAe,IAAI,CAAC,EACnF,EAEIyB,EAAkBjC,CAAG,EACvB,MAAMA,EAER,OAAO,MAAMD,EAAgBC,CAAG,CAClC,CAEA,IAAMkC,EAAM,CACV,GAAApC,EACA,IAAAY,EACA,GAAImB,EAAO,CAAE,KAAMA,CAAK,EAAI,CAAC,EAC7B,GAAIlB,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,EAC/B,GAAII,EAAe,CAAE,aAAAA,CAAa,EAAI,CAAC,EACvC,GAAIM,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAII,EAAW,CAAE,SAAAA,CAAS,EAAI,CAAC,CACjC,EAQA,GAAI,CACF,IAAMU,EAAW,MAAMxC,EAAUuC,EAAK3C,CAAG,EAEzC,OAAAiB,EAAe,KAAK,EACpBZ,EAAI,KACF,yBAAoBE,CAAE,8BAA8BU,EAAe,IAAI,CAAC,EAC1E,EAEO2B,CACT,OAASnC,EAAK,CAGZ,GAFAQ,EAAe,KAAK,EAEhByB,EAAkBjC,CAAG,EACvB,MAAAJ,EAAI,KAAK,4CAAkC,EAC3CA,EAAI,KACF,yBAAoBE,CAAE,8BAA8BU,EAAe,IAAI,CAAC,EAC1E,EAEMR,EAGR,OAAAJ,EAAI,MACF,4BAAqBE,CAAE,4BAA4BU,EAAe,IAAI,CAAC,GACzE,EACO,MAAMT,EAAgBC,CAAG,CAClC,CACF,CACF,CChWO,IAAMoC,EAAN,cAA2B,KAAM,CAC7B,KACA,IAET,YAAYC,EAAcC,EAA+B,CACvD,MAAM,gCAAgC,EAEtC,KAAK,KAAOD,EACZ,KAAK,IAAMC,EAEX,KAAK,KAAO,cACd,CACF,ECIO,IAAMC,EAAoB,0BAqC1B,SAASC,EAKdC,EACAC,EAC0E,CAC1E,IAAMC,EAAMC,EAAaH,EAAQ,KAAK,EAChCI,EAAKJ,EAAQ,IAAMF,EAEnBO,EAAYL,EAAQ,YAAc,SAAS,IAE3CM,EACJC,IAEAL,EAAI,MAAM,gDAAyCE,CAAE,IAAKG,CAAG,EAEzDC,EAAcD,CAAG,EACZ,CACL,QAASA,EAAI,QACb,KAAMA,EAAI,IACZ,EAGK,CACL,QAAS,KAAK,UAAUA,CAAG,CAC7B,GAGIE,EAA2BF,IAC/BL,EAAI,MACF,kDAA2CE,CAAE,2DAC/C,EACOE,EAAiBC,CAAG,GAGvBG,EAAUV,EAAQ,SAAWM,EAE7BK,EACJC,IAEAV,EAAI,MAAM,8CAAuCE,CAAE,IAAKQ,CAAM,EACvD,CACL,OAAAA,CACF,GAGIC,EACJb,EAAQ,wBAA0BW,EAEpC,OAAO,eACLG,EACiE,CACjE,IAAMC,EAAiBC,EAAqB,EAC5CD,EAAe,MAAM,EAErBb,EAAI,KAAK,oCAA6BE,CAAE,GAAG,EAE3C,IAAIa,EACJ,GAAIjB,EAAQ,MAAO,CACjB,IAAIkB,EACAJ,aAAyB,SAC3BI,EAAe,OAAO,YAAYJ,EAAc,QAAQ,CAAC,EAEzDI,EAAeJ,EAGjB,IAAMK,EAAcC,EAAmBpB,EAAQ,MAAOkB,CAAY,EAClE,GAAIC,EAAY,OAYd,MAAO,CACL,QAAS,GACT,MAAO,CACL,KAAM,mBACN,IAfQ,MAAME,EAChB,IAAMR,EAAuBM,EAAY,MAAM,EAC/C,KACEjB,EAAI,MACF,iEAA0DE,CAAE,mGAC9D,EAEOO,EAAgCQ,EAAY,MAAM,EAE7D,CAOE,CACF,EAGFF,EAAQE,EAAY,KACtB,CAEA,IAAIG,EACJ,GAAI,CACF,IAAMC,EAAa,CACjB,GAAAnB,EACA,GAAIa,EAAQ,CAAE,MAAAA,CAAM,EAAI,CAAC,CAC3B,EAEAK,EAAO,MAAMjB,EAAUkB,CAAU,CACnC,OAAShB,EAAc,CAOrB,GANAQ,EAAe,KAAK,EAEpBb,EAAI,MACF,4BAAqBE,CAAE,0BAA0BW,EAAe,IAAI,CAAC,EACvE,EAEIS,EAAkBjB,CAAG,EACvB,MAAMA,EAQR,MAAO,CACL,QAAS,GACT,MAAO,CACL,KAAM,qBACN,IATQ,MAAMc,EAChB,IAAMX,EAAQH,CAAG,EACjB,IAAME,EAAwBF,CAAG,CACnC,CAOE,CACF,CACF,CAEA,IAAMkB,EAAM,CACV,GAAArB,EACA,OAAQ,CAACsB,EAAcD,IAAyC,CAC9D,MAAM,IAAIE,EAAaD,EAAMD,CAAG,CAClC,EACA,GAAIH,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAIL,EAAQ,CAAE,MAAAA,CAAM,EAAI,CAAC,CAC3B,EAEA,GAAI,CACF,IAAMW,EAAS,MAAM3B,EAAQwB,CAAG,EAEhC,OAAAV,EAAe,KAAK,EACpBb,EAAI,KACF,yBAAoBE,CAAE,8BAA8BW,EAAe,IAAI,CAAC,EAC1E,EAEO,CACL,QAAS,GACT,OAAAa,CACF,CACF,OAASrB,EAAc,CAGrB,GAFAQ,EAAe,KAAK,EAEhBS,EAAkBjB,CAAG,EACvB,MAAAL,EAAI,KAAK,gEAAsDE,CAAE,GAAG,EACpEF,EAAI,KACF,yBAAoBE,CAAE,8BAA8BW,EAAe,IAAI,CAAC,EAC1E,EACMR,EAGR,OAAIA,aAAeoB,GACjBzB,EAAI,KACF,yBAAoBE,CAAE,8BAA8BW,EAAe,IAAI,CAAC,EAC1E,EACO,CACL,QAAS,GACT,MAAO,CACL,KAAMR,EAAI,KACV,IAAKA,EAAI,GACX,CACF,IAGFL,EAAI,MACF,4BAAqBE,CAAE,6BAA6BW,EAAe,IAAI,CAAC,EAC1E,EAOO,CACL,QAAS,GACT,MAAO,CACL,KAAM,eACN,IATQ,MAAMM,EAChB,IAAMX,EAAQH,CAAG,EACjB,IAAME,EAAwBF,CAAG,CACnC,CAOE,CACF,EACF,CACF,CACF","names":["DEFAULT_ID","readRequestBodyAsJson","req","text","createSafeRouteHandler","options","handlerFn","log","createLogger","id","onErrorResponse","err","onSegmentsValidationErrorResponse","issues","onSearchParamsValidationErrorResponse","onBodyValidationErrorResponse","onFormDataValidationErrorResponse","authorize","providedContext","executionClock","createExecutionClock","url","segments","params","parsedSegments","parseWithDictionary","searchParams","queryParams_unsafe","k","values","parsedSearchParams","clonedReq_forBody","body","body_unsafe","parsedBody","validateWithSchema","formData","contentType","formData_unsafe","parsedFormData","auth","clonedReq_forAuth","authParams","authOrResponse","isNextNativeError","ctx","response","KTaggedError","code","ctx","DEFAULT_ACTION_ID","createSafeServerAction","options","handler","log","createLogger","id","authorize","onError_fallback","err","isNativeError","onError_fallbackNoThrow","onError","onInputValidationError_fallback","issues","onInputValidationError","providedInput","executionClock","createExecutionClock","input","input_unsafe","parsedInput","validateWithSchema","assertsNoThrow","auth","authParams","isNextNativeError","ctx","code","KTaggedError","output"]}
|
|
@@ -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(); } }var
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }var _chunkMJPPHUFPcjs = require('../chunk-MJPPHUFP.cjs');var S=class extends Error{constructor(s,t,e){super(`${s==="searchParams"?"Search params":"Segments"} validation error for ${e} server component '${t}`),this.name="ValidationError"}},g=class extends Error{constructor(s,t){super(`No segments provided for ${t} server component '${s}'`),this.name="NoSegmentsProvidedError"}},h=class extends Error{constructor(s,t){super(`No search params provided for ${t} server component '${s}'`),this.name="NoSearchParamsProvidedError"}},C=class extends Error{constructor(s,t){let e=t.map(f=>`'${f}'`).join(", ");super(`Missing slots [${e}] for layout server component '${s}'`),this.name="MissingLayoutSlotsError"}};var L="[unknown:page:server:component]";function A(r,s){let t=_chunkMJPPHUFPcjs.a.call(void 0, r.debug),e=_nullishCoalesce(r.id, () => (L)),f=_nullishCoalesce(r.onError, () => ((u=>{throw t.error(`\u{1F6D1} Unexpected error in page server component '${e}'`,u),u}))),P=_nullishCoalesce(r.onSegmentsValidationError, () => ((u=>{throw t.error(`\u{1F6D1} Invalid segments for page server component '${e}'`,u),new S("segments",e,"page")}))),y=_nullishCoalesce(r.onSearchParamsValidationError, () => ((u=>{throw t.error(`\u{1F6D1} Invalid search params for server component '${e}'`,u),new S("searchParams",e,"page")}))),p=_nullishCoalesce(r.authorize, () => ((async()=>{})));return async function(l){let i=_chunkMJPPHUFPcjs.b.call(void 0, );i.start(),t.info(`\u{1F504} Running page server component'${e}'`);let a;if(r.segments){let o=await l.params;if(o===void 0)throw new g(e,"page");let n=_chunkMJPPHUFPcjs.g.call(void 0, r.segments,o);n.issues?await P(n.issues):a=n.value}let c;if(r.searchParams){let o=await l.searchParams;if(o===void 0)throw new h(e,"page");let n=_chunkMJPPHUFPcjs.g.call(void 0, r.searchParams,o);n.issues?await y(n.issues):c=n.value}let d;try{let o={id:e,...a?{segments:a}:{},...c?{searchParams:c}:{}};d=await p(o)}catch(o){throw i.stop(),t.error(`\u{1F6D1} Page server component '${e}' not authorized after ${i.get()}`),o}try{let o={id:e,...d?{auth:d}:{},...a?{segments:a}:{},...c?{searchParams:c}:{}},n=await s(o);return i.stop(),t.info(`\u2705 Page server component '${e}' executed successfully in ${i.get()}`),n}catch(o){if(i.stop(),_chunkMJPPHUFPcjs.c.call(void 0, o))throw t.info(`\u2139\uFE0F Ignoring native Next.js error while executing page server component '${e}' after ${i.get()}`),o;return t.error(`\u{1F6D1} Page server component '${e}' failed to execute after ${i.get()}`),await f(o)}}}var E="[unknown:layout:server:component]";function D(r,s){let t=_chunkMJPPHUFPcjs.a.call(void 0, r.debug),e=_nullishCoalesce(r.id, () => (E)),f=_nullishCoalesce(r.onError, () => ((p=>{throw t.error(`\u{1F6D1} Unexpected error in layout server component '${e}'`,p),p}))),P=_nullishCoalesce(r.onSegmentsValidationError, () => ((p=>{throw t.error(`\u{1F6D1} Invalid segments for layout server component '${e}'`,p),new S("segments",e,"page")}))),y=_nullishCoalesce(r.authorize, () => ((async()=>{})));return async function({params:u,children:l,...i}){let a=_chunkMJPPHUFPcjs.b.call(void 0, );a.start(),t.info(`\u{1F504} Running layout server component'${e}'`);let c;if(r.segments){let n=await u;if(n===void 0)throw new g(e,"page");let m=_chunkMJPPHUFPcjs.g.call(void 0, r.segments,n);m.issues?await P(m.issues):c=m.value}let d;if(r.experimental_slots){let n=r.experimental_slots,m=[];for(let $ of n)$ in i||m.push($);if(m.length>0)throw new C(e,m);d=i}let o;try{let n={id:e,...c?{segments:c}:{}};o=await y(n)}catch(n){throw a.stop(),t.error(`\u{1F6D1} Layout server component '${e}' not authorized after ${a.get()}`),n}try{let n={id:e,...o?{auth:o}:{},...c?{segments:c}:{},children:l,...d?{experimental_slots:d}:{}},m=await s(n);return a.stop(),t.info(`\u2705 Layout server component '${e}' executed successfully in ${a.get()}`),m}catch(n){if(a.stop(),_chunkMJPPHUFPcjs.c.call(void 0, n))throw t.info(`\u2139\uFE0F Ignoring native Next.js error while executing layout server component '${e}' after ${a.get()}`),n;return t.error(`\u{1F6D1} Layout server component '${e}' failed to execute after ${a.get()}`),await f(n)}}}exports.createSafeLayoutServerComponent = D; exports.createSafePageServerComponent = A;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|