Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "valsan",
"version": "2.1.1",
"version": "2.2.0",
"description": "Validation and sanitization library for TypeScript",
"private": "true",
"typescript-template": {
Expand Down
2 changes: 1 addition & 1 deletion src/primitives/network/fqdn-valsan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class FqdnValSan extends ValSan<string, string> {
}

protected override async normalize(input: string): Promise<string> {
return input?.trim();
return typeof input === 'string' ? input.trim() : input;
}

protected async validate(input: string): Promise<ValidationResult> {
Expand Down
2 changes: 1 addition & 1 deletion src/primitives/network/ip-address-valsan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class IpAddressValSan extends ValSan<string, string> {
}

protected override async normalize(input: string): Promise<string> {
return input?.trim();
return typeof input === 'string' ? input.trim() : input;
}

protected async validate(input: string): Promise<ValidationResult> {
Expand Down
2 changes: 1 addition & 1 deletion src/primitives/network/mac-address-valsan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class MacAddressValSan extends ValSan<string, string> {
}

protected override async normalize(input: string): Promise<string> {
return input?.trim();
return typeof input === 'string' ? input.trim() : input;
}

protected async validate(input: string): Promise<ValidationResult> {
Expand Down
2 changes: 1 addition & 1 deletion src/primitives/network/url-valsan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class UrlValSan extends ValSan<string, string> {
}

protected override async normalize(input: string): Promise<string> {
return input?.trim();
return typeof input === 'string' ? input.trim() : input;
}

protected async validate(input: string): Promise<ValidationResult> {
Expand Down
7 changes: 4 additions & 3 deletions src/primitives/number/is-numeric.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export function isNumeric(value: unknown): boolean {
return (
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'bigint'
(typeof value === 'number' ||
typeof value === 'bigint' ||
(typeof value === 'string' && value.length > 0)) &&
!Number.isNaN(Number(value))
);
}
32 changes: 32 additions & 0 deletions src/valsan-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ValSanTypes } from './types/types';
import { SanitizeResult, ValSanOptions } from './valsan';

export class BaseValSan<TInput = unknown, TOutput = TInput> {
public type: ValSanTypes = 'unknown';
public example = '';
public format?: string;

public options: ValSanOptions;

public checkRequired(input: unknown): SanitizeResult<TOutput> {
if (this.options.isOptional) {
return {
success: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: input as any,
errors: [],
};
}
else {
return {
success: false,
errors: [
{
code: 'required',
message: 'Value is required',
},
],
};
}
}
}
33 changes: 20 additions & 13 deletions src/valsan-composed.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { RuleSet } from './rules/rule';
import { ValSanTypes } from './types/types';
import {
RunsLikeAValSan as RunsLikeAValSan,
SanitizeResult,
ValSanOptions,
} from './valsan';
import { BaseValSan } from './valsan-base';

export interface ComposedValSanOptions extends ValSanOptions {
/**
Expand Down Expand Up @@ -41,10 +41,8 @@ export interface ComposedValSanOptions extends ValSanOptions {
* ```
*/
export class ComposedValSan<TInput = unknown, TOutput = TInput>
implements RunsLikeAValSan<TInput, TOutput> {
public type: ValSanTypes = 'unknown';
public example = '';

extends BaseValSan<TInput, TOutput>
implements RunsLikeAValSan<TInput, TOutput> {
/**
* Creates a composed validator from an array of ValSan steps.
*
Expand All @@ -55,8 +53,10 @@ implements RunsLikeAValSan<TInput, TOutput> {
constructor(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public readonly steps: RunsLikeAValSan<any, any>[],
public readonly options: ComposedValSanOptions = {}
public override readonly options: ComposedValSanOptions = {}
) {
super();

if (steps.length === 0) {
throw new Error('ComposedValSan requires at least one step');
}
Expand All @@ -71,6 +71,18 @@ implements RunsLikeAValSan<TInput, TOutput> {
return [...this.steps];
}

public copy(
options: ComposedValSanOptions
): ComposedValSan<TInput, TOutput> {
const constructor = this.constructor as new (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
steps: RunsLikeAValSan<any, any>[],
options: ComposedValSanOptions
) => ComposedValSan<TInput, TOutput>;

return new constructor(this.steps, { ...this.options, ...options });
}

public rules(): RuleSet {
const combinedRules: RuleSet = {};

Expand All @@ -89,13 +101,8 @@ implements RunsLikeAValSan<TInput, TOutput> {

async run(input: TInput): Promise<SanitizeResult<TOutput>> {
// Handle optional fields
const isOptional = this.options.isOptional;
if (isOptional && (input === undefined || input === null)) {
return {
success: true,
data: input as unknown as TOutput,
errors: [],
};
if (input === undefined || input === null) {
return this.checkRequired(input);
}

let value: TInput | TOutput = input;
Expand Down
36 changes: 20 additions & 16 deletions src/valsan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { validationError } from './errors';
import { Rule } from './rules';
import { RuleSet } from './rules/rule';
import { ValSanTypes } from './types/types';
import { BaseValSan } from './valsan-base';

export interface ValidationError {
field?: string;
Expand Down Expand Up @@ -41,20 +42,28 @@ export interface RunsLikeAValSan<TInput = unknown, TOutput = TInput> {
}

export abstract class ValSan<
TInput = unknown,
TOutput = TInput,
TNormalized = TInput | TOutput,
> implements RunsLikeAValSan<TInput, TOutput> {
public constructor(public readonly options: ValSanOptions = {}) {}

public type: ValSanTypes = 'unknown';
public example = '';
public format?: string;
TInput = unknown,
TOutput = TInput,
TNormalized = TInput | TOutput,
>
extends BaseValSan<TInput, TOutput>
implements RunsLikeAValSan<TInput, TOutput> {
public constructor(public override readonly options: ValSanOptions = {}) {
super();
}

public rules(): RuleSet {
return {};
}

public copy(options: ValSanOptions): ValSan<TInput, TOutput, TNormalized> {
const constructor = this.constructor as new (
options: ValSanOptions
) => ValSan<TInput, TOutput, TNormalized>;

return new constructor({ ...this.options, ...options });
}

/**
* Optional normalization step applied before validation.
*/
Expand All @@ -67,13 +76,8 @@ export abstract class ValSan<

public async run(input: TInput): Promise<SanitizeResult<TOutput>> {
// Handle optional fields
const isOptional = this.options.isOptional;
if (isOptional && (input === undefined || input === null)) {
return {
success: true,
data: input as unknown as TOutput,
errors: [],
};
if (input === undefined || input === null) {
return this.checkRequired(input);
}

// Apply normalization before validation
Expand Down
10 changes: 9 additions & 1 deletion test/spec/primitives/auth/bearer-token-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('BearerTokenValSan', () => {
const validator = new BearerTokenValSan();
const result = await validator.run(undefined);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
expect(result.errors[0].code).toBe('required');
});

it('rejects string without Bearer prefix', async () => {
Expand All @@ -50,4 +50,12 @@ describe('BearerTokenValSan', () => {
expect(result.success).toBe(true);
expect(result.data).toBe('mF_9.B5f-4.1JqM');
});

it('rejects array input', async () => {
const validator = new BearerTokenValSan();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await validator.run(['Bearer token'] as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
});
});
9 changes: 8 additions & 1 deletion test/spec/primitives/date-time/iso8601-timestamp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Iso8601TimestampValSan', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(undefined as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string_or_date');
expect(result.errors[0].code).toBe('required');
});

it('should sanitize a Date to Date', async () => {
Expand All @@ -52,4 +52,11 @@ describe('Iso8601TimestampValSan', () => {
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('iso8601');
});

it('should reject invalid Date objects', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(2 as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string_or_date');
});
});
7 changes: 7 additions & 0 deletions test/spec/primitives/network/fqdn-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,11 @@ describe('FqdnValSan', () => {
const result = await valSan.run('label-.example.com');
expect(result.success).toBe(false);
});

it('rejects non-string input', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(123 as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
});
});
7 changes: 7 additions & 0 deletions test/spec/primitives/network/ip-address-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,11 @@ describe('IpAddressValSan', () => {
const result = await valSan.run('2001:db8::1');
expect(result.success).toBe(false);
});

it('rejects non-string input', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(123 as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
});
});
7 changes: 7 additions & 0 deletions test/spec/primitives/network/mac-address-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ describe('MacAddressValSan', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(undefined as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('required');
});

it('rejects non-string input', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(123 as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
});
});
5 changes: 5 additions & 0 deletions test/spec/primitives/network/port-number-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ describe('PortNumberValSan', () => {
expect(result.success).toBe(false);
});

it('rejects non-numeric string', async () => {
const result = await valSan.run('invalidPort');
expect(result.success).toBe(false);
});

it('rejects port > 65535', async () => {
const result = await valSan.run(70000);
expect(result.success).toBe(false);
Expand Down
7 changes: 7 additions & 0 deletions test/spec/primitives/network/url-valsan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ describe('UrlValSan', () => {
const result = await valSan.run('https://exa mple.com');
expect(result.success).toBe(false);
});

it('rejects non-string input', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await valSan.run(123 as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('string');
});
});
10 changes: 9 additions & 1 deletion test/spec/primitives/number/integer-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('IntegerValidator', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await validator.run(undefined as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('number');
expect(result.errors[0].code).toBe('required');
});

it('should reject decimal numbers', async () => {
Expand All @@ -51,4 +51,12 @@ describe('IntegerValidator', () => {
expect(result.success).toBe(true);
expect(result.data).toBe(1000000);
});

it('should reject non-number input', async () => {
const validator = new IntegerValidator();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await validator.run('not a number' as any);
expect(result.success).toBe(false);
expect(result.errors[0].code).toBe('number');
});
});
Loading