Skip to content

Commit

Permalink
feat(theme:http): add @Payload decorator (#686)
Browse files Browse the repository at this point in the history
- feat(theme:http): use `::id` to indicate escaping
- close ng-alain/ng-alain#1317
  • Loading branch information
cipchk committed Sep 24, 2019
1 parent 3752205 commit 123c29e
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 23 deletions.
63 changes: 63 additions & 0 deletions packages/theme/src/services/http/http.decorator.spec.ts
Expand Up @@ -18,6 +18,7 @@ import {
POST,
PUT,
Query,
Payload,
} from './http.decorator';

@BaseUrl('/user')
Expand All @@ -38,6 +39,11 @@ class MockService extends BaseApi {
return null as any;
}

@GET('::id/:id/::id')
escapePath(@Path('id') _id: number | undefined): Observable<any> {
return null as any;
}

@GET('')
arrQS(@Query('ids') _ids: number[]): Observable<any> {
return null as any;
Expand All @@ -48,6 +54,16 @@ class MockService extends BaseApi {
return null as any;
}

@GET('')
payloadGet(@Payload _query: any, @Query('status') _status?: number): Observable<any> {
return null as any;
}

@POST(':id')
payloadPost(@Payload _body: any, @Body _body2?: {}): Observable<any> {
return null as any;
}

@DELETE()
DELETE(): Observable<any> {
return null as any;
Expand Down Expand Up @@ -188,6 +204,20 @@ describe('theme: http.decorator', () => {
expect(request.calls.mostRecent().args[1]).toBe('/user');
});
});

it('should be escaping operations', () => {
srv.escapePath(10);

expect(request).toHaveBeenCalled();
expect(request.calls.mostRecent().args[1]).toContain(`:id/10/:id`);
});

it('should be ingore replace param when is invalid value', () => {
srv.escapePath(undefined);

expect(request).toHaveBeenCalled();
expect(request.calls.mostRecent().args[1]).toContain(`:id/:id/:id`);
});
});

it('should construct a POST request', () => {
Expand All @@ -204,6 +234,39 @@ describe('theme: http.decorator', () => {
});
});

describe('PAYLOAD', () => {
it('should be get', () => {
srv.payloadGet({ pi: 1, ps: 10 });
expect(request).toHaveBeenCalled();
const arg = request.calls.mostRecent().args[2];
expect(arg.params.pi).toBe(1);
expect(arg.params.ps).toBe(10);
});
it('should be merge Query & Payload when method is get', () => {
srv.payloadGet({ pi: 13, ps: 14 }, 520);
expect(request).toHaveBeenCalled();
const arg = request.calls.mostRecent().args[2];
expect(arg.params.pi).toBe(13);
expect(arg.params.ps).toBe(14);
expect(arg.params.status).toBe(520);
});
it('should be post', () => {
srv.payloadPost({ pi: 1, ps: 10 });
expect(request).toHaveBeenCalled();
const arg = request.calls.mostRecent().args[2];
expect(arg.body.pi).toBe(1);
expect(arg.body.ps).toBe(10);
});
it('should be merge Body & Payload when method is post', () => {
srv.payloadPost({ pi: 13, ps: 14 }, { woc: 520 });
expect(request).toHaveBeenCalled();
const arg = request.calls.mostRecent().args[2];
expect(arg.body.pi).toBe(13);
expect(arg.body.ps).toBe(14);
expect(arg.body.woc).toBe(520);
});
});

describe('[acl]', () => {
it('should be request when user authorized', () => {
tokens.ACLService = {
Expand Down
60 changes: 39 additions & 21 deletions packages/theme/src/services/http/http.decorator.ts
Expand Up @@ -7,7 +7,7 @@ import { throwError, Observable } from 'rxjs';
import { _HttpClient } from './http.client';

export abstract class BaseApi {
constructor(@Inject(Injector) protected injector: Injector) { }
constructor(@Inject(Injector) protected injector: Injector) {}
}

export interface HttpOptions {
Expand Down Expand Up @@ -41,7 +41,7 @@ function setParam(target: any, key = paramKey) {
* - 有效范围:类
*/
export function BaseUrl(url: string) {
return function <TClass extends new (...args: any[]) => BaseApi>(target: TClass): TClass {
return function<TClass extends new (...args: any[]) => BaseApi>(target: TClass): TClass {
const params = setParam(target.prototype);
params.baseUrl = url;
return target;
Expand All @@ -56,19 +56,19 @@ export function BaseHeaders(
headers:
| HttpHeaders
| {
[header: string]: string | string[];
},
[header: string]: string | string[];
},
) {
return function <TClass extends new (...args: any[]) => BaseApi>(target: TClass): TClass {
return function<TClass extends new (...args: any[]) => BaseApi>(target: TClass): TClass {
const params = setParam(target.prototype);
params.baseHeaders = headers;
return target;
};
}

function makeParam(paramName: string) {
return function (key?: string, ...extraOptions: any[]) {
return function (target: BaseApi, propertyKey: string, index: number) {
return function(key?: string) {
return function(target: BaseApi, propertyKey: string, index: number) {
const params = setParam(setParam(target), propertyKey);
let tParams = params[paramName];
if (typeof tParams === 'undefined') {
Expand All @@ -77,7 +77,6 @@ function makeParam(paramName: string) {
tParams.push({
key,
index,
...extraOptions,
});
};
};
Expand Down Expand Up @@ -108,17 +107,29 @@ export const Body = makeParam('body')();
*/
export const Headers = makeParam('headers');

/**
* Request Payload
* - Supported body (like`POST`, `PUT`) as a body data, equivalent to `@Body`
* - Not supported body (like `GET`, `DELETE` etc) as a `QueryString`
*/
export const Payload = makeParam('payload')();

function getValidArgs(data: any, key: string, args: any[]): {} {
if (!data[key] || !Array.isArray(data[key]) || data[key].length <= 0) {
return {};
}
return args[data[key][0].index];
}

function makeMethod(method: string) {
return function (url: string = '', options?: HttpOptions) {
return function(url: string = '', options?: HttpOptions) {
return (_target: BaseApi, targetKey?: string, descriptor?: PropertyDescriptor) => {
descriptor!.value = function (...args: any[]): Observable<any> {
descriptor!.value = function(...args: any[]): Observable<any> {
options = options || {};

const http = this.injector.get(_HttpClient, null);
const http = this.injector.get(_HttpClient, null) as _HttpClient;
if (http == null) {
throw new TypeError(
`Not found '_HttpClient', You can import 'AlainThemeModule' && 'HttpClientModule' in your root module.`,
);
throw new TypeError(`Not found '_HttpClient', You can import 'AlainThemeModule' && 'HttpClientModule' in your root module.`);
}

const baseData = setParam(this);
Expand All @@ -143,23 +154,30 @@ function makeMethod(method: string) {
delete options.acl;
}

(data.path || []).forEach((i: ParamType) => {
requestUrl = requestUrl.replace(new RegExp(`:${i.key}`, 'g'), encodeURIComponent(args[i.index]));
});
requestUrl = requestUrl.replace(/::/g, '^^');
((data.path as ParamType[]) || [])
.filter(w => typeof args[w.index] !== 'undefined')
.forEach((i: ParamType) => {
requestUrl = requestUrl.replace(new RegExp(`:${i.key}`, 'g'), encodeURIComponent(args[i.index]));
});
requestUrl = requestUrl.replace(/\^\^/g, `:`);

const params = (data.query || []).reduce((p, i: ParamType) => {
const params = (data.query || []).reduce((p: {}, i: ParamType) => {
p[i.key] = args[i.index];
return p;
}, {});

const headers = (data.headers || []).reduce((p, i: ParamType) => {
const headers = (data.headers || []).reduce((p: {}, i: ParamType) => {
p[i.key] = args[i.index];
return p;
}, {});

const payload = getValidArgs(data, 'payload', args);
const supportedBody = method === 'POST' || method === 'PUT';

return http.request(method, requestUrl, {
body: data.body && data.body.length > 0 ? args[data.body[0].index] : null,
params,
body: supportedBody ? { ...getValidArgs(data, 'body', args), ...payload } : null,
params: !supportedBody ? { ...params, ...payload } : params,
headers: { ...baseData.baseHeaders, ...headers },
...options,
});
Expand Down
23 changes: 22 additions & 1 deletion packages/theme/src/services/http/index.en-US.md
Expand Up @@ -78,7 +78,20 @@ class RestService extends BaseApi {
}

@GET(':id')
GET(@Path('id') id: number): Observable<any> {
get(@Path('id') id: number): Observable<any> {
return;
}

@GET()
get(@Payload data: {}): Observable<any> {
return;
}

// Use `::id` to indicate escaping, and should be will be ignored when `id` value is `undefined`, like this:
  // When `id` is `10` => 10:type
  // When `id` is `undefined` => :id:type
@GET(':id::type')
get(@Path('id') id: number): Observable<any> {
return;
}

Expand All @@ -87,6 +100,11 @@ class RestService extends BaseApi {
return;
}

@POST()
save(@Payload data: {}): Observable<any> {
return;
}

// If authorization is invalid, will be thrown directly `401` error and will not be sent.
@GET('', { acl: 'admin' })
ACL(): Observable<any> {
Expand Down Expand Up @@ -127,3 +145,6 @@ class RestService extends BaseApi {
- `@Query(key?: string)` QueryString of URL
- `@Body` Body of URL
- `@Headers(key?: string)` Headers of URL
- `@Payload` Request Payload
- Supported body (like`POST`, `PUT`) as a body data, equivalent to `@Body`
- Not supported body (like `GET`, `DELETE` etc) as a `QueryString`
23 changes: 22 additions & 1 deletion packages/theme/src/services/http/index.zh-CN.md
Expand Up @@ -78,7 +78,20 @@ class RestService extends BaseApi {
}

@GET(':id')
GET(@Path('id') id: number): Observable<any> {
get(@Path('id') id: number): Observable<any> {
return;
}

@GET()
get(@Payload data: {}): Observable<any> {
return;
}

// 使用 `::id` 来表示转义,若 `id` 值为 `undefined` 会忽略转换,例如:
// 当 `id` 为 `10` 时 => 10:type
// 当 `id` 为 `undefined` 时 => :id:type
@GET(':id::type')
get(@Path('id') id: number): Observable<any> {
return;
}

Expand All @@ -87,6 +100,11 @@ class RestService extends BaseApi {
return;
}

@POST()
save(@Payload data: {}): Observable<any> {
return;
}

// 若请求的URL不符合授权要求,会直接抛出 `401` 错误,且不发送请求
@GET('', { acl: 'admin' })
ACL(): Observable<any> {
Expand Down Expand Up @@ -127,3 +145,6 @@ class RestService extends BaseApi {
- `@Query(key?: string)` URL 参数 QueryString
- `@Body` 参数 Body
- `@Headers(key?: string)` 参数 Headers
- `@Payload` 请求负载
- 当支持 Body 时(例如:`POST``PUT`)为内容体等同 `@Body`
- 当不支持 Body 时(例如:`GET``DELETE` 等)为 `QueryString`

0 comments on commit 123c29e

Please sign in to comment.