Skip to content

Commit

Permalink
1.5.2
Browse files Browse the repository at this point in the history
- `soft` versions can now also return the parsed value if there was one returned but it did not pass the validation. This is useful when you want to show the user the parsed value in the form after a validation error.
  • Loading branch information
miunau committed Apr 3, 2024
1 parent dd57258 commit 2362e60
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 61 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.5.2 (2024-04-03)

- `soft` versions can now also return the parsed value if there was one returned but it did not pass the validation. This is useful when you want to show the user the parsed value in the form after a validation error.

## 1.5.1 (2024-04-03)

- Added missing export for `BodyguardFormConfig` interface
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ Below are the methods and types available in the Bodyguard class.
- `maxFilenameLength?`: `number` - Maximum allowed length of a filename in the body. Default: `255`
- `allowedContentTypes?`: `string[]` - Allowed content types for file uploads. Default: `undefined`

#### `BodyguardResult<T> = BodyguardSuccess<T> | BodyguardError`
#### `BodyguardResult<T> = BodyguardSuccess<T> | BodyguardError<T>`

- `success`: `boolean` - Whether the parsing was successful.
- `error?`: `Error` - The error that occurred, if any.
Expand All @@ -328,10 +328,11 @@ Below are the methods and types available in the Bodyguard class.
- `success`: `true`
- `value`: `T`

#### `BodyguardError`
#### `BodyguardError<T>`

- `success`: `false`
- `error`: `Error`
- `value?`: `T`

#### `BodyguardValidator<T = JSONLike> = (obj: JSONLike) => T`

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@auth70/bodyguard",
"version": "1.5.1",
"version": "1.5.2",
"description": "Fetch API compatible streaming JSON and form data body parser and guard",
"keywords": [
"request",
Expand Down
162 changes: 115 additions & 47 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,37 @@ export class Bodyguard {
}
}

private async formInternal<
K extends JSONLike = JSONLike
> (
input: Request | Response,
config?: Partial<BodyguardFormConfig>,
): Promise<K> {
if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});

const contentType = input.headers.get("content-type");
if (!contentType || contentType === '') throw new Error(ERRORS.NO_CONTENT_TYPE);

const bodyType = contentType === "application/x-www-form-urlencoded" ? "params" : "formdata";

let boundary = "";
if(contentType.includes("boundary")) {
const match = contentType.match(/boundary=(.*)/);
if (!match || !match[1]) {
throw new Error(ERRORS.INVALID_CONTENT_TYPE);
}
boundary = match[1];
}

if(bodyType === "formdata" && !boundary) throw new Error(ERRORS.INVALID_CONTENT_TYPE);

const parser = bodyType === "params" ? new URLParamsParser(instanceConfig) : new FormDataParser(instanceConfig as BodyguardFormConfig, boundary);
const ret = await parser.parse(input.body);

return ret as K;
}

/**
* Attempts to parse a form from a Request or Response. Returns the parsed object in case of success and
* an error object in case of failure.
Expand All @@ -126,10 +157,24 @@ export class Bodyguard {
config?: Partial<BodyguardFormConfig>
): Promise<BodyguardResult<K>> {
try {
const res = await this.form(input, validator, config);
return {
success: true,
value: res as K
const ret = await this.formInternal(input, config);
try {
if(validator) {
return {
success: true,
value: await Promise.resolve(validator(ret)) as K
}
}
return {
success: true,
value: ret as K
}
} catch(err) {
return {
success: false,
error: err,
value: ret as K
}
}
} catch(e: any) {
return {
Expand All @@ -144,6 +189,7 @@ export class Bodyguard {
* @param {Request | Response} input - Request or Response to parse the form from.
* @param {BodyguardValidator} validator - Optional validator to validate the parsed form against.
* @param {Partial<BodyguardFormConfig>} config - Optional configuration to override the default configuration.
* @param {boolean} soft - Whether to throw an error or return an error object in case of failure.
* @return {Promise<K>} - Parsed form from the Request or Response.
* @throws {Error} - If content-type is not present or is invalid, or the form data is invalid, it throws an error.
*/
Expand All @@ -153,36 +199,27 @@ export class Bodyguard {
> (
input: Request | Response,
validator?: T,
config?: Partial<BodyguardFormConfig>
config?: Partial<BodyguardFormConfig>,
soft?: boolean
): Promise<K> {
if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});

const contentType = input.headers.get("content-type");
if (!contentType || contentType === '') throw new Error(ERRORS.NO_CONTENT_TYPE);

const bodyType = contentType === "application/x-www-form-urlencoded" ? "params" : "formdata";

let boundary = "";
if(contentType.includes("boundary")) {
const match = contentType.match(/boundary=(.*)/);
if (!match || !match[1]) {
throw new Error(ERRORS.INVALID_CONTENT_TYPE);
}
boundary = match[1];
}

if(bodyType === "formdata" && !boundary) throw new Error(ERRORS.INVALID_CONTENT_TYPE);

const parser = bodyType === "params" ? new URLParamsParser(instanceConfig) : new FormDataParser(instanceConfig as BodyguardFormConfig, boundary);
const ret = await parser.parse(input.body);

const ret = await this.formInternal(input, config);
if(validator) {
return await Promise.resolve(validator(ret)) as K;
}

return ret as K;
}

private async jsonInternal<
K extends JSONLike = JSONLike
> (
input: Request | Response,
config?: Partial<BodyguardConfig>
): Promise<K> {
if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});
const parser = new JSONParser(instanceConfig);
const ret = await parser.parse(input.body);
return ret as K;
}

/**
Expand All @@ -202,10 +239,24 @@ export class Bodyguard {
config?: Partial<BodyguardConfig>
): Promise<BodyguardResult<K>> {
try {
const res = await this.json(input, validator, config);
return {
success: true,
value: res as K
const ret = await this.jsonInternal(input, config);
try {
if(validator) {
return {
success: true,
value: await Promise.resolve(validator(ret)) as K
}
}
return {
success: true,
value: ret as K
}
} catch(err) {
return {
success: false,
error: err,
value: ret as K
}
}
} catch(e: any) {
return {
Expand All @@ -231,17 +282,23 @@ export class Bodyguard {
validator?: T,
config?: Partial<BodyguardConfig>
): Promise<K> {

if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});

const parser = new JSONParser(instanceConfig);
const ret = await parser.parse(input.body);

const ret = await this.jsonInternal(input, config);
if(validator) {
return await Promise.resolve(validator(ret)) as K;
}
return ret as K;
}

private async textInternal<
K extends JSONLike = JSONLike
> (
input: Request | Response,
config?: Partial<BodyguardConfig>
): Promise<K> {
if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});
const parser = new TextParser(instanceConfig);
const ret = await parser.parse(input.body);
return ret as K;
}

Expand All @@ -262,10 +319,24 @@ export class Bodyguard {
config?: Partial<BodyguardConfig>
): Promise<BodyguardResult<K>> {
try {
const res = await this.text(input, validator, config);
return {
success: true,
value: res as K
const ret = await this.textInternal(input, config);
try {
if(validator) {
return {
success: true,
value: await Promise.resolve(validator(ret)) as K
}
}
return {
success: true,
value: ret as K
}
} catch(err) {
return {
success: false,
error: err,
value: ret as K
}
}
} catch(e: any) {
return {
Expand All @@ -291,10 +362,7 @@ export class Bodyguard {
validator?: T,
config?: Partial<BodyguardConfig>
): Promise<K> {
if(!input.body) throw new Error(ERRORS.BODY_NOT_AVAILABLE);
const instanceConfig = this.constructConfig(config || {});
const parser = new TextParser(instanceConfig);
const ret = await parser.parse(input.body);
const ret = await this.textInternal(input, config);
if(validator) {
return await Promise.resolve(validator(ret)) as K;
}
Expand Down
24 changes: 13 additions & 11 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,29 @@ export interface BodyguardFormConfig extends BodyguardConfig {
allowedContentTypes: string[] | undefined;
}

export type BodyguardError = {
export type JSONLike =
| { [property: string]: JSONLike }
| readonly JSONLike[]
| string
| number
| boolean
| File
| null;

export type BodyguardError<T = JSONLike> = {
success: false;
/** The error message */
error: unknown;
/** The value that was being processed. May be undefined if the error occurred before the value was processed. */
value?: T;
};

export type BodyguardSuccess<T = JSONLike> = {
success: true;
value: T;
};

export type JSONLike =
| { [property: string]: JSONLike }
| readonly JSONLike[]
| string
| number
| boolean
| File
| null;

export type BodyguardResult<SuccessType = JSONLike> = BodyguardSuccess<SuccessType> | BodyguardError;
export type BodyguardResult<T = JSONLike> = BodyguardSuccess<T> | BodyguardError<T>;

/*
* Utility functions
Expand Down

0 comments on commit 2362e60

Please sign in to comment.