Skip to content

Commit

Permalink
feat: inject into data-cleaner 4.2 namespace export
Browse files Browse the repository at this point in the history
BREAKING CHANGE: injecting into data-cleaner is now opttional and doesn't go into root export
  • Loading branch information
IlyaSemenov committed Jan 3, 2023
1 parent cc317a3 commit 08c9d23
Show file tree
Hide file tree
Showing 11 changed files with 437 additions and 109 deletions.
3 changes: 3 additions & 0 deletions packages/data-cleaner-koa/.taprc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage: false
node-arg: ["-r", "@swc-node/register"]
test-regex: "\\.test\\.ts$"
65 changes: 46 additions & 19 deletions packages/data-cleaner-koa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ The cleaner is configured with the following schema options:
- `files`: files cleaner (optional)
- `errorCode`: HTTP status code for the failed validation response (default: 200)

## Example
## Example (User Registration)

Consider a registration form:
Client-side 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 }}
<div>Login: <input name="username" required /></div>
<div>Password: <input name="password" type="password" required /></div>
<div>Company name: <input name="company.name" required /></div>
<div>Domain: <input name="company.domain" required />.mysaas.com</div>
<div><button>Register</button></div>
<div>errors = {{ errors }}</div>
</form>
</template>
Expand Down Expand Up @@ -53,19 +53,19 @@ export default {
</script>
```

and the corresponding backend:
Koa backend using `data-cleaner-koa`:

```js
import * as clean from 'data-cleaner'
import 'data-cleaner-koa' // injects into data-cleaner
import { clean } from 'data-cleaner'
import { cleanKoa } from 'data-cleaner-koa'

const cleanRegister = clean.koa({
const cleanRegister = cleanKoa({
body: clean.object({
parseKeys: true,
fields: {
username: clean.string({
async clean (username: string) {
const user = await User.query().select('id').where('username', username).first()
const user = await User.query().select('id').findOne({ username })
if (user) {
throw new clean.ValidationError('This username is not available.')
}
Expand All @@ -81,7 +81,7 @@ const cleanRegister = clean.koa({
domain = domain.toLowerCase()
if (
domain.match(/^(www|mail|admin)/) ||
await Company.query().select('id').where('domain', domain).first()
await Company.query().select('id').findOne({ domain })
) {
throw new clean.ValidationError('This domain is not available.')
}
Expand Down Expand Up @@ -115,15 +115,42 @@ router.post('/register', async ctx => {
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'>
interface RegisterFields {
username: string
password: string
company: {
name: string
domain: string
}
}

const cleanRegister = clean.koa<RegisterFields>({
...
const cleanRegister = cleanKoa<RegisterFields>({
body: clean.object({ ... }),
})

const { body } = await cleanRegister(ctx) // body is a RegisterFields object
router.get(async ctx => {
const { body } = await cleanRegister(ctx) // body is a RegisterFields object
})
```

The `files` are currently untyped.

## Experimental namespace injection

It's possible to use `clean.koa` instead of `cleanKoa`:

```ts
// Somewhere during app init:
import "data-cleaner-koa/register"

// Later use clean.koa:
import { clean } from "data-cleaner"

const mycleaner = clean.koa({
body: clean.object.fields({
username: clean.string(),
}),
})
```

Note that this probably breaks tree shaking.
36 changes: 29 additions & 7 deletions packages/data-cleaner-koa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,53 @@
},
"author": "Ilya Semenov",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"source": "src/index.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./register": {
"require": "./dist/register.js",
"import": "./dist/register.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"dist/index.d.ts"
],
"register": [
"dist/register.d.ts"
]
}
},
"files": [
"dist/**",
"src/**"
],
"scripts": {
"build": "tsup",
"prepack": "npm run build",
"test": "echo No tests so far."
"test": "tap"
},
"dependencies": {
"http-errors": "^2.0.0"
},
"peerDependencies": {
"data-cleaner": "^3 | ^4"
"data-cleaner": "^4.2.0"
},
"devDependencies": {
"@swc-node/register": "~1.5.4",
"@swc/core": "~1.3.24",
"@types/formidable": "~2.0.5",
"@types/http-errors": "~2.0.1",
"@types/koa": "^2.0.48",
"@types/tap": "~15.0.7",
"data-cleaner": "workspace:*",
"data-cleaner-koa": ".",
"koa-body": "^4.1.0",
"tsup": "~6.5.0"
"tap": "~16.3.2",
"tsup": "~6.5.0",
"typescript": "~4.9.4"
}
}
63 changes: 63 additions & 0 deletions packages/data-cleaner-koa/src/clean-koa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { cleanAny, Cleaner, ValidationError } from "data-cleaner"
import { Files } from "formidable"
import createError from "http-errors"
import Koa from "koa"

export interface CleanKoaSchema<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 function cleanKoa<
BodyT = any,
StateT = any,
ContextT extends Koa.ParameterizedContext<StateT> = Koa.ParameterizedContext<StateT>,
ReturnT = CleanKoaRequest<BodyT>
>(schema: CleanKoaSchema<BodyT, ReturnT>): Cleaner<ReturnT, ContextT> {
return cleanAny<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 ValidationError) {
const http_error = createError<number>(
400,
JSON.stringify({ errors: err.messages || err.errors }),
{
headers: { "Content-Type": "application/json" },
}
)
if (schema.errorCode) {
// avoid createError deprecation warning for status < 400
http_error.status = schema.errorCode
}
// 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
}
},
})
}
70 changes: 1 addition & 69 deletions packages/data-cleaner-koa/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1 @@
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
export * from "./clean-koa"
12 changes: 12 additions & 0 deletions packages/data-cleaner-koa/src/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { clean } from "data-cleaner"

import { cleanKoa } from "./clean-koa"

declare module "data-cleaner" {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace clean {
let koa: typeof cleanKoa
}
}

clean.koa = cleanKoa
11 changes: 11 additions & 0 deletions packages/data-cleaner-koa/tests/register.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "data-cleaner-koa/register"

import { clean } from "data-cleaner"
import tap from "tap"

tap.test("simple", async (tap) => {
const clean_request = clean.koa({ body: clean.integer() })
tap.strictSame(await clean_request({ request: { body: 123 } } as any), {
body: 123,
})
})
10 changes: 10 additions & 0 deletions packages/data-cleaner-koa/tests/simple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { clean } from "data-cleaner"
import { cleanKoa } from "data-cleaner-koa"
import tap from "tap"

tap.test("simple", async (tap) => {
const clean_request = cleanKoa({ body: clean.integer() })
tap.strictSame(await clean_request({ request: { body: 123 } } as any), {
body: 123,
})
})
2 changes: 1 addition & 1 deletion packages/data-cleaner-koa/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": ["src"],
"include": ["src", "tests"],
"compilerOptions": {
"types": ["koa-body"]
}
Expand Down
2 changes: 1 addition & 1 deletion packages/data-cleaner-koa/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"

export default defineConfig({
clean: true,
entry: ["src/index.ts"],
entry: ["src/index.ts", "src/register.ts"],
format: ["cjs", "esm"],
sourcemap: true,
dts: true,
Expand Down
Loading

0 comments on commit 08c9d23

Please sign in to comment.