-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(data-cleaner-koa): initial monorepo release
- Loading branch information
1 parent
a02f652
commit c9faaef
Showing
7 changed files
with
745 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# data-cleaner-koa | ||
|
||
This is a plugin for [node-data-cleaner](https://github.com/IlyaSemenov/node-data-cleaner) for _cleaning_ Koa.js requests (or more precisely, for `koa-body@4` requests). | ||
|
||
It will return cleaned `body` and `files`, and throw a Koa HTTP error response with `{ "errors": ... }` if there are validation errors. | ||
|
||
## Schema options | ||
|
||
The cleaner creator accepts the following schema options: | ||
|
||
- `body`: body cleaner (optional) | ||
- `files`: files cleaner (optional) | ||
- `errorCode`: HTTP status code for the failed validation response (default: 200) | ||
|
||
## Example | ||
|
||
Consider a registration form: | ||
|
||
```vue | ||
<template> | ||
<form ref="form" @submit.prevent="submit"> | ||
Login: <input name="username" required /> Password: | ||
<input name="password" type="password" required /> Company name: | ||
<input name="company.name" required /> Domain: | ||
<input name="company.domain" required />.mysaas.com | ||
<button>Register</button> | ||
errors = {{ errors }} | ||
</form> | ||
</template> | ||
<script> | ||
export default { | ||
data() { | ||
return { | ||
errors: null, | ||
} | ||
}, | ||
methods: { | ||
async submit() { | ||
this.errors = null | ||
const { data } = await this.$axios.post( | ||
'/register', | ||
new FormData(this.$refs.form), | ||
) | ||
if (data.errors) { | ||
this.errors = data.errors | ||
} else { | ||
await this.$store.dispatch('login', data.user) | ||
} | ||
}, | ||
}, | ||
} | ||
</script> | ||
``` | ||
|
||
and the corresponding backend: | ||
|
||
```js | ||
import * as clean from 'data-cleaner' | ||
import 'data-cleaner-koa' // injects into data-cleaner | ||
|
||
const cleanRegister = clean.koa({ | ||
body: clean.object({ | ||
parseKeys: true, | ||
fields: { | ||
username: clean.string({ | ||
async clean (username: string) { | ||
const user = await User.query().select('id').where('username', username).first() | ||
if (user) { | ||
throw new clean.ValidationError('This username is not available.') | ||
} | ||
return username | ||
}, | ||
}), | ||
password: clean.string(), | ||
company: clean.object({ | ||
fields: { | ||
name: clean.string(), | ||
domain: clean.string({ | ||
async clean (domain: string) { | ||
domain = domain.toLowerCase() | ||
if ( | ||
domain.match(/^(www|mail|admin)/) || | ||
await Company.query().select('id').where('domain', domain).first() | ||
) { | ||
throw new clean.ValidationError('This domain is not available.') | ||
} | ||
return domain | ||
}, | ||
}), | ||
}, | ||
}), | ||
}, | ||
}), | ||
}) | ||
|
||
router.post('/register', async ctx => { | ||
const { body } = await cleanRegister(ctx) | ||
const user = await User.query().upsertGraphAndFetch({ | ||
username: body.username, // will be unique (*) | ||
password: body.password, | ||
company: { | ||
name: body.company.name, | ||
domain: body.company.domain, // will be lowercase | ||
}, | ||
}), | ||
ctx.body = user | ||
}) | ||
``` | ||
|
||
- _NOTE: There is a race condition during unique username check, it's not handled for simplicity. For production use, wrap everything into a database transaction._ | ||
|
||
## Typescript | ||
|
||
In the example above, `cleanKoa` will accept optional return value interface for body fields: | ||
|
||
```ts | ||
interface RegisterFields extends Pick<IUser, 'username' | 'password'> { | ||
company: Pick<ICompany, 'name' | 'domain'> | ||
} | ||
|
||
const cleanRegister = clean.koa<RegisterFields>({ | ||
... | ||
}) | ||
|
||
const { body } = await cleanRegister(ctx) // body is a RegisterFields object | ||
``` | ||
|
||
The `files` are currently untyped. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "data-cleaner-koa", | ||
"version": "0.0.0-development", | ||
"description": "data-cleaner plugin for Koa.js requests", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/IlyaSemenov/node-data-cleaner.git" | ||
}, | ||
"author": "Ilya Semenov", | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"types": "dist/index.d.ts", | ||
"source": "src/index.ts", | ||
"files": [ | ||
"dist/**", | ||
"src/**" | ||
], | ||
"scripts": { | ||
"build": "tsup", | ||
"prepack": "npm run build", | ||
"test": "echo No tests so far." | ||
}, | ||
"dependencies": { | ||
"http-errors": "^1.7.2" | ||
}, | ||
"peerDependencies": { | ||
"data-cleaner": "^3 | ^4" | ||
}, | ||
"devDependencies": { | ||
"@types/formidable": "~2.0.5", | ||
"@types/http-errors": "^1.8.2", | ||
"@types/koa": "^2.0.48", | ||
"data-cleaner": "workspace:*", | ||
"koa-body": "^4.1.0", | ||
"tsup": "5.11.13" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import * as clean from 'data-cleaner' | ||
import { Cleaner } from 'data-cleaner' | ||
import { Files } from 'formidable' | ||
import createError from 'http-errors' | ||
import Koa from 'koa' | ||
|
||
export interface KoaSchema<BodyT, ReturnT> { | ||
body?: Cleaner<BodyT, any> // TODO: any -> type of ctx.request.body | ||
files?: Cleaner<Files> | ||
clean?: Cleaner<ReturnT, CleanKoaRequest<BodyT>> | ||
errorCode?: number | ||
} | ||
|
||
export interface CleanKoaRequest<BodyT> { | ||
body: BodyT | ||
files: Files // TODO: allow this to be optional | ||
} | ||
|
||
export default function clean_koa< | ||
BodyT = any, | ||
StateT = any, | ||
ContextT extends Koa.ParameterizedContext<StateT> = Koa.ParameterizedContext<StateT>, | ||
ReturnT = CleanKoaRequest<BodyT>, | ||
>(schema: KoaSchema<BodyT, ReturnT>): Cleaner<ReturnT, ContextT> { | ||
return clean.any<ReturnT, ContextT>({ | ||
async clean(ctx, opts) { | ||
try { | ||
let res: any = {} | ||
if (schema.body) { | ||
res.body = await schema.body(ctx.request.body, opts) | ||
} | ||
if (schema.files) { | ||
res.files = await schema.files(ctx.request.files, opts) | ||
} | ||
if (schema.clean) { | ||
res = await schema.clean(res, opts) | ||
} | ||
return res | ||
} catch (err) { | ||
if (err instanceof clean.ValidationError) { | ||
const http_error = createError<number>( | ||
400, | ||
JSON.stringify({ errors: err.messages || err.errors }), | ||
{ | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
) | ||
// avoid createError deprecation warning for status < 400 | ||
http_error.status = schema.errorCode || 200 | ||
// prevent Koa from resetting content-type, see https://github.com/koajs/koa/issues/787 | ||
Object.defineProperty(ctx.response, 'type', { | ||
set() { | ||
// do nothing. | ||
}, | ||
}) | ||
throw http_error | ||
} | ||
throw err | ||
} | ||
}, | ||
}) | ||
} | ||
|
||
declare module 'data-cleaner' { | ||
export const koa: typeof clean_koa | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
require('data-cleaner').koa = clean_koa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "@tsconfig/node14/tsconfig.json", | ||
"include": ["src"], | ||
"compilerOptions": { | ||
"types": ["koa-body"] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { defineConfig } from 'tsup' | ||
|
||
export default defineConfig({ | ||
clean: true, | ||
entry: ['src/index.ts'], | ||
format: ['cjs', 'esm'], | ||
sourcemap: true, | ||
dts: true, | ||
external: ['data-cleaner', 'formidable', 'http-errors', 'koa', 'koa-body'], | ||
}) |
Oops, something went wrong.