Skip to content

Commit

Permalink
feat: parse array query by qs (#3751)
Browse files Browse the repository at this point in the history
* feat: add qs for parse query with array

* fix: lint

* test: add case
  • Loading branch information
czy88840616 committed May 6, 2024
1 parent f2a8f5e commit ca5a5d6
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/web-koa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
"@midwayjs/core": "^3.15.8",
"@midwayjs/session": "^3.15.8",
"@types/koa": "2.15.0",
"@types/qs": "6.9.14",
"koa": "2.15.3",
"koa-bodyparser": "4.4.1"
"koa-bodyparser": "4.4.1",
"qs": "6.11.2"
},
"author": "Harry Chen <[email protected]>",
"repository": {
Expand Down
45 changes: 45 additions & 0 deletions packages/web-koa/src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type { DefaultState, Middleware, Next } from 'koa';
import * as koa from 'koa';
import { Server } from 'http';
import { setupOnError } from './onerror';
import * as qs from 'qs';
import * as querystring from 'querystring';

const COOKIES = Symbol('context#cookies');

Expand Down Expand Up @@ -127,6 +129,49 @@ export class MidwayKoaFramework extends BaseFramework<
},
});

const converter =
this.configurationOptions.queryParseMode === 'strict'
? function (value) {
return !Array.isArray(value) ? [value] : value;
}
: this.configurationOptions.queryParseMode === 'first'
? function (value) {
return Array.isArray(value) ? value[0] : value;
}
: undefined;

// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// fix query with array params
Object.defineProperty(this.app.request, 'query', {
get() {
const str = this.querystring;
const c = (this._querycache = this._querycache || {});

// find cache
if (c[str]) return c[str];

if (self.configurationOptions.queryParseMode) {
// use qs module to parse query
c[str] = qs.parse(
str,
self.configurationOptions.queryParseOptions || {}
);
} else {
// use querystring to parse query by default
c[str] = querystring.parse(str);
}

if (converter) {
for (const key in c[str]) {
c[str][key] = converter(c[str][key]);
}
}

return c[str];
},
});

const onerrorConfig = this.configService.getConfiguration('onerror');
setupOnError(this.app, onerrorConfig, this.logger);

Expand Down
9 changes: 9 additions & 0 deletions packages/web-koa/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IConfigurationOptions, IMidwayApplication, IMidwayContext } from '@midw
import * as koa from 'koa';
import { Context as KoaContext, DefaultState, Middleware, Next } from 'koa';
import { RouterParamValue } from '@midwayjs/core';
import * as qs from 'qs';

export interface State extends DefaultState {}

Expand Down Expand Up @@ -83,6 +84,14 @@ export interface IMidwayKoaConfigurationOptions extends IConfigurationOptions {
*/
serverTimeout?: number;
/**
* qs mode
*/
queryParseMode?: 'extended' | 'strict' | 'first';
/**
* qs options
*/
queryParseOptions?: qs.IParseOptions;
/*
* https/https/http2 server options
*/
serverOptions?: Record<string, any>;
Expand Down
133 changes: 132 additions & 1 deletion packages/web-koa/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { closeApp, creatApp, createHttpRequest } from './utils';
import { IMidwayKoaApplication } from '../src';
import { makeHttpRequest } from '@midwayjs/core';
import { Controller, Get, makeHttpRequest } from '@midwayjs/core';
import { createLightApp } from '@midwayjs/mock';

describe('/test/feature.test.ts', () => {

Expand Down Expand Up @@ -479,4 +480,134 @@ describe('/test/feature.test.ts', () => {

await closeApp(app);
});

describe('test query parser', () => {
@Controller()
class HomeController {
@Get('/query')
async query(ctx) {
return ctx.query;
}
}
it('should test parse with querystring', async () => {
const app = await createLightApp('', {
imports: [
require('../src'),
],
preloadModules: [
HomeController
],
globalConfig: {
keys: '123'
}
});

let result = await createHttpRequest(app)
.get('/query?a=1&b=2&a=3&c[0]=1&c[1]=2');

expect(result.text).toEqual(JSON.stringify({'a': ['1', '3'], 'b': '2', 'c[0]': '1', 'c[1]': '2'}));

await closeApp(app);
});

it('should test parse with parseQueryMode extended', async () => {
const app = await createLightApp('', {
imports: [
require('../src'),
],
preloadModules: [
HomeController
],
globalConfig: {
keys: '123',
koa: {
queryParseMode: 'extended'
}
}
});

let result = await createHttpRequest(app)
.get('/query?a=1&b=2&a=3&c[0]=1&c[1]=2');

expect(result.text).toEqual(JSON.stringify({'a': ['1', '3'], 'b': '2', 'c': ['1', '2']}));

await closeApp(app);
});

it('should test parse with parseQueryMode strict', async () => {
const app = await createLightApp('', {
imports: [
require('../src'),
],
preloadModules: [
HomeController
],
globalConfig: {
keys: '123',
koa: {
queryParseMode: 'strict'
}
}
});

let result = await createHttpRequest(app)
.get('/query?a=1&b=2&a=3&c[0]=1&c[1]=2');

expect(result.text).toEqual(JSON.stringify({'a': ['1', '3'], 'b': ['2'], 'c': ['1', '2']}));

await closeApp(app);
});

it('should test parse with parseQueryMode first', async () => {
const app = await createLightApp('', {
imports: [
require('../src'),
],
preloadModules: [
HomeController
],
globalConfig: {
keys: '123',
koa: {
queryParseMode: 'first'
}
}
});

let result = await createHttpRequest(app)
.get('/query?a=1&b=2&a=3&c[0]=1&c[1]=2');

expect(result.text).toEqual(JSON.stringify({'a': '1', 'b': '2', 'c': '1'}));

await closeApp(app);
});

it('should test queryParseOptions', async () => {
const app = await createLightApp('', {
imports: [
require('../src'),
],
preloadModules: [
HomeController
],
globalConfig: {
keys: '123',
koa: {
queryParseMode: 'first',
queryParseOptions: {
parameterLimit: 3,
arrayLimit: 1,
}
}
}
});

let result = await createHttpRequest(app)
.get('/query?a=1&b=2&a=3&c[0]=1&c[1]=2');

expect(result.text).toEqual(JSON.stringify({'a': '1', 'b': '2'}));

await closeApp(app);
});
});
});
79 changes: 79 additions & 0 deletions site/docs/extensions/koa.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,85 @@ export default {
```


### Query 数组解析

默认情况下,koa 使用 `querystring` 解析 query 参数,当碰到数组时,会将数组的数据拆开。

比如:

```
GET /query?a[0]=1&a[1]=2
```

拿到的结果是:

```json
{
"a[0]": 1,
"a[1]": 2,
}
```

框架提供了一些参数来处理这种情况。

```typescript
// src/config/config.default
export default {
// ...
koa: {
queryParseMode: 'extended',
// ...
},
}
```

`queryParseMode` 参数可以选择 `extended``strict``first` 三种值。

`queryParseMode` 有值时,会使用 `qs` 模块处理 query,效果同 `koa-qs` 模块。

当请求参数为 `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'` 时。

默认效果(使用 `querystring`

```JSON
{
"a": ["1", "3" ],
"b": "2",
"c[0]": "1",
"c[1]": "2"
}
```

`extended` 效果

```JSON
{
"a": ["1", "3" ],
"b": ["2"],
"c": ["1", "2"]
}
```

`strict` 效果

```JSON
{
"a": ["1", "3" ],
"b": "2",
"c": ["1", "2"]
}
```

`first` 效果

```JSON
{
"a": "1",
"b": "2",
"c": "1"
}
```


### 超时配置

Expand Down

0 comments on commit ca5a5d6

Please sign in to comment.