Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
TomHermsen committed Jul 1, 2024
1 parent b35523f commit 3f3f932
Show file tree
Hide file tree
Showing 14 changed files with 431 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Release

on:
push:
tags:
- 'v*.*.*'

jobs:
release:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org/'

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test

- name: Build project
run: npm run build

- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
node_modules
dist
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
src
tsconfig.json
jest.config.js
tests
build.config.ts
.github
77 changes: 77 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Typescript Validator

`ts-validator` is a TypeScript library for validating data against specified rules. It supports validation for
various data types, nested objects, and arrays.

## Installation

You can install the package via npm:

```bash
npm install ts-validator
```

Or with yarn
```bash
yarn add ts-validator
```

## Rules

The validator supports the following rules:

required: The field must be present.
string: The field must be a string.
number: The field must be a number.
array: The field must be an array.
min:<number>: The field must be a string with a minimum length of <number>.

More coming soon!

## Examples
### Nested object validation
```typescript
const validator = Validator.create({
'user.id': ['required', 'string'],
'user.name': ['required', 'string'],
'user.age': ['required', 'number'],
})

const data = {user: {id: '1', name: 'John Doe', age: 20}}

try {
validator.validateObject<{
user: {
id: string,
name: string,
age: number,
}
}>(data)
console.log('Validation passed')
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed', error.errors)
}
}

```

### Nested object validation
```typescript
const validator = Validator.create({
'tags': ['array'],
'tags.*': ['string']
})

const data = {tags: ['tag1', 'tag2']}

try {
validator.validateObject<{ tags: string[] }>(data)
console.log('Validation passed')
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed', error.errors)
}
}

```
13 changes: 13 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {defineBuildConfig} from 'unbuild'

export default defineBuildConfig({
entries: ['./src/index'],
outDir: 'dist',
declaration: true,
rollup: {
esbuild: {
minify: true,
},
emitCJS: true,
}
})
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.ts'],
}
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "checkmate-ts",
"version": "1.0.0",
"description": "Checkmate Typescript - An object validator written in Typescript",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"test": "jest"
},
"author": {
"name": "Tom Hermsen",
"url": "https://github.com/tomhermsen"
},
"keywords": [
"typescript",
"validation",
"typed",
"data",
"dto",
"request"
],
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.5",
"typescript": "^5.5.2",
"unbuild": "^2.0.0"
}
}
116 changes: 116 additions & 0 deletions src/Validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {Rule} from './types/Rule'
import type {Rules} from './types/Rules'
import {ValidationError} from './exceptions/ValidationError'

class Validator {
private rules: Rules

constructor(rules: Rules) {
this.rules = rules
}

static create(rules: Rules) {
return new this(rules)
}

private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => {
if (part === '*') {
if (!Array.isArray(acc)) {
return undefined
}
return acc
}
return acc && acc[part]
}, obj)
}

private validateField(key: string, value: any, rules: Rule[], errors: { [key: string]: string[] }) {
rules.forEach(rule => {
if (rule === 'required' && (value === undefined || value === null)) {
if (!errors[key]) {
errors[key] = []
}
errors[key].push(`${key} is required`)
}

if (rule === 'string' && typeof value !== 'string') {
if (!errors[key]) {
errors[key] = []
}
errors[key].push(`${key} is not a string`)
}

if (rule === 'number' && !Number.isSafeInteger(Number(value))) {
if (!errors[key]) {
errors[key] = []
}
errors[key].push(`${key} is not a number`)
}

if (rule === 'array' && !Array.isArray(value)) {
if (!errors[key]) {
errors[key] = []
}
errors[key].push(`${key} is not an array`)
}

if (rule.startsWith('min:')) {
const minLength = parseInt(rule.split(':')[1], 10)
if (typeof value === 'string' && value.length < minLength) {
if (!errors[key]) {
errors[key] = []
}
errors[key].push(`${key} should be at least ${minLength} characters long`)
}
}
})
}

private validateArray(key: string, array: any[], errors: { [key: string]: string[] }) {
array.forEach((item, index) => {
Object.entries(this.rules).forEach(([ruleKey, ruleValues]) => {
const arrayRuleKey = ruleKey.replace('.*', `[${index}]`)
if (arrayRuleKey.startsWith(`${key}[${index}]`)) {
const nestedKey = arrayRuleKey.slice(`${key}[${index}].`.length)
const nestedValue = this.getNestedValue(array, `${index}${nestedKey ? '.' + nestedKey : ''}`)
this.validateField(`${key}[${index}]${nestedKey ? '.' + nestedKey : ''}`, nestedValue, ruleValues, errors)
}
})
})
}

private validate<T>(data: object): T {
let errors: { [key: string]: string[] } = {}
Object.entries(this.rules).forEach(([key, rules]) => {
const value = this.getNestedValue(data, key)

if (key.includes('.*')) {
const arrayKey = key.split('.*')[0]
const arrayValue = this.getNestedValue(data, arrayKey)
if (Array.isArray(arrayValue)) {
this.validateArray(arrayKey, arrayValue, errors)
} else {
if (!errors[arrayKey]) {
errors[arrayKey] = []
}
errors[arrayKey].push(`${arrayKey} is not an array`)
}
} else {
this.validateField(key, value, rules, errors)
}
})

if (Object.keys(errors).length) {
throw new ValidationError(errors)
}

return data as T
}

validateObject<T>(data: object): T {
return this.validate<T>(data)
}
}

export {Validator}
12 changes: 12 additions & 0 deletions src/exceptions/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class ValidationError extends Error {
errors: Record<string, string[]> = {}

constructor(errors: Record<string, string[]>, message: string = 'Validation error') {
super(message)
this.errors = errors

Object.setPrototypeOf(this, ValidationError.prototype)
}
}

export {ValidationError}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Rule } from './types/Rule'
import type { Rules } from './types/Rules'
import { Validator } from './Validator'

export { Rule, Rules, Validator }
1 change: 1 addition & 0 deletions src/types/Rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Rule = 'required' | 'string' | `min:${number}` | 'number' | 'array';
3 changes: 3 additions & 0 deletions src/types/Rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type {Rule} from './Rule'

export type Rules = { [key: string]: Rule[] };
Loading

0 comments on commit 3f3f932

Please sign in to comment.