Skip to content

Commit

Permalink
feat(common): add $ctx.response.cookie method to set cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
Romakita committed Jan 16, 2023
1 parent 746d105 commit 71b5fe5
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 54 deletions.
148 changes: 128 additions & 20 deletions docs/docs/request-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ prev: true
next: true
---

# Request context
# Context

Ts.ED provides an util to get request, response, to store and share data along all middlewares/endpoints during a
request with @@PlatformContext@@. This context is created by Ts.ED when the request is handled by the server.
Expand Down Expand Up @@ -110,12 +110,16 @@ class PlatformRequest<T = Req> {

get url(): string;

get route(): string;

get headers(): IncomingHttpHeaders;

get method(): string;

get body(): {[key: string]: any};

get rawBody(): {[key: string]: any};

get cookies(): {[key: string]: any};

get params(): {[key: string]: any};
Expand All @@ -125,7 +129,43 @@ class PlatformRequest<T = Req> {
get session(): {[key: string]: any} | undefined;

get(name: string): string | undefined; // get header
getHeader(name: string): string | undefined; // get header
accepts(mime?: string | string[]): string | string[] | false;
isAborted(): boolean;
}
```

### Get request headers

```typescript
import {Controller, Context} from "@tsed/common";

@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
ctx.request.headers; // return all headers
ctx.get("host"); // return host header
ctx.getHeader("host"); // return host header
}
}
```

### Get request params/body/query/cookies/session

```typescript
import {Controller, Context} from "@tsed/common";

@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
ctx.request.body; // return body payload
ctx.request.params; // return path params
ctx.request.query; // return query params
ctx.request.cookies; // return cookies
ctx.request.session; // return session
}
}
```

Expand Down Expand Up @@ -164,37 +204,105 @@ class PlatformResponse {
onEnd(cb: Function): void;

destroy(): void;

cookie(name: string, value: string | null, opts?: TsED.SetCookieOpts): this;
}
```

Example:
### Set response headers

```typescript
import {Middleware, Context} from "@tsed/common";
import {Controller, Context} from "@tsed/common";

@Middleware()
export class MyMiddleware {
use(@Context() ctx: Context) {
@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
// set headers, content-type and status
ctx.setHeaders({"x-header": "header"});
ctx.contentType("application/json");
ctx.status(201);
ctx.response.setHeaders({"x-header": "header"});
ctx.response.contentType("application/json");
ctx.response.status(201);
}
}
```

// equivalent to ctx.response.raw.send()
ctx.body(null);
ctx.body(undefined);
ctx.body(true);
ctx.body(false);
Can be also done by returning a response like object:

// equivalent to ctx.response.raw.json()
ctx.body({});
ctx.body([]);
ctx.body(new Date());
```typescript
import {Controller, Context} from "@tsed/common";

@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
return {
statusText: "OK",
status: 200,
headers: {},
data: {}
};
}
}
```

### Set response cookie

```typescript
import {Controller, Context} from "@tsed/common";

@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
// set
ctx.response.cookie("locale", "fr-FR");

// clear
ctx.response.cookie("locale", null);
}
}
```

### Set response body

```typescript
import {Controller, Context} from "@tsed/common";

@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
// equivalent to ctx.getResponse().send()
ctx.response.body(null);
ctx.response.body(undefined);
ctx.response.body(true);
ctx.response.body(false);

// equivalent to ctx.getResponse().json()
ctx.response.body({});
ctx.response.body([]);
ctx.response.body(new Date());

// equivalent to readableStream.pipe(ctx.response.raw)
ctx.body(readableStream);
ctx.response.body(readableStream);
}
}
```

But prefer returning payload from your method! Ts.ED will handle all data type (Buffer/Stream/Data/Promise/Observable).

### Manipulate original response

You can retrieve the Express\Koa response by using `ctx.getResponse()` method:

```typescript
import {Controller, Context} from "@tsed/common";

// use raw response
@Controller("/")
export class MyController {
@Get("/")
get(@Context() ctx: Context) {
// Express.js
ctx.getResponse<Express.Response>().status(201).send("Hello");
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/domain/AnyToPromise.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {isObject} from "../utils/objects/isObject";
import {isObservable} from "../utils/objects/isObservable";
import {isPromise} from "../utils/objects/isPromise";
import {isStream} from "../utils/objects/isStream";
Expand All @@ -6,7 +7,7 @@ import {isStream} from "../utils/objects/isStream";
* @ignore
*/
function isResponse(obj: any) {
return obj.data && obj.headers && obj.status && obj.statusText;
return isObject(obj) && "data" in obj && "headers" in obj && "status" in obj && "statusText" in obj;
}

/**
Expand Down
30 changes: 25 additions & 5 deletions packages/orm/adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"fs-extra": "10.0.1",
"lowdb": "1.0.0",
"tslib": "2.4.0",
"uuid": "8.3.2"
"uuid": "8.3.2",
"change-case": "^4.1.2",
"lodash": "^4.17.21"
},
"devDependencies": {
"@tsed/ajv": "7.12.0",
Expand All @@ -43,8 +45,26 @@
"@tsed/core": "^7.12.0",
"@tsed/di": "^7.12.0",
"@tsed/json-mapper": "^7.12.0",
"@tsed/schema": "^7.12.0",
"change-case": "^4.1.2",
"lodash": "^4.17.21"
"@tsed/schema": "^7.12.0"
},
"peerDependenciesMeta": {
"@tsed/ajv": {
"optional": false
},
"@tsed/common": {
"optional": false
},
"@tsed/core": {
"optional": false
},
"@tsed/di": {
"optional": false
},
"@tsed/json-mapper": {
"optional": false
},
"@tsed/schema": {
"optional": false
}
}
}
}
2 changes: 1 addition & 1 deletion packages/platform/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@
}
},
"devDependencies": {}
}
}
2 changes: 2 additions & 0 deletions packages/platform/common/src/services/PlatformRequest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ describe("PlatformRequest", () => {
expect($ctx.request.raw).toEqual(request);
expect($ctx.request.response).toEqual($ctx.response);
expect($ctx.request.headers).toEqual({});
expect($ctx.request.get("host")).toEqual(undefined);
expect($ctx.request.getHeader("host")).toEqual(undefined);
expect($ctx.request.method).toEqual("GET");
});

Expand Down
4 changes: 4 additions & 0 deletions packages/platform/common/src/services/PlatformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export class PlatformRequest<Req extends {[key: string]: any} = any> {
return this.raw.get(name);
}

getHeader(name: string) {
return this.get(name);
}

/**
* Checks if the specified content types are acceptable, based on the request’s Accept HTTP header field. The method returns the best match, or if none of the specified content types is acceptable, returns false (in which case, the application should respond with 406 "Not Acceptable").
*
Expand Down
39 changes: 39 additions & 0 deletions packages/platform/common/src/services/PlatformResponse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {PlatformTest} from "@tsed/common";
import {isArray} from "@tsed/core";
import {PlatformViews} from "@tsed/platform-views";
import {createReadStream} from "fs";
import {PlatformResponse} from "./PlatformResponse";
Expand Down Expand Up @@ -243,4 +244,42 @@ describe("PlatformResponse", () => {
expect(res.on).toHaveBeenCalledWith("finish", cb);
});
});
describe("cookie()", () => {
it("should manipulate cookies", () => {
const {res, response} = createResponse();

res.cookie = (...args: any[]) => {
let value = `${args[0]}=${args[1]}`;

if (!res.headers["set-cookie"]) {
res.headers["set-cookie"] = value;
} else {
res.headers["set-cookie"] = [].concat(res.headers["set-cookie"], value as any);
}
};

res.clearCookie = (name: string) => {
res.headers["set-cookie"] = res.headers["set-cookie"].filter((cookie: string) => cookie.startsWith(name + "="));
};

response.cookie("locale", "fr-FR");
expect(res.headers).toEqual({
"set-cookie": ["locale=fr-FR"],
"x-request-id": "id"
});

response.cookie("filename", "test");
expect(res.headers).toEqual({
"set-cookie": ["locale=fr-FR", "filename=test"],
"x-request-id": "id"
});

response.cookie("filename", null);

expect(res.headers).toEqual({
"set-cookie": ["filename=test"],
"x-request-id": "id"
});
});
});
});
31 changes: 30 additions & 1 deletion packages/platform/common/src/services/PlatformResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {isBoolean, isNumber, isStream, isString} from "@tsed/core";
import {isArray, isBoolean, isNumber, isStream, isString} from "@tsed/core";
import {Injectable, ProviderScope, Scope} from "@tsed/di";
import {OutgoingHttpHeaders, ServerResponse} from "http";
import type {PlatformContext} from "../domain/PlatformContext";
Expand All @@ -8,6 +8,18 @@ declare global {
namespace TsED {
// @ts-ignore
export interface Response {}

export interface SetCookieOpts extends Record<string, unknown> {
maxAge?: number | undefined;
signed?: boolean | undefined;
expires?: Date | undefined;
httpOnly?: boolean | undefined;
path?: string | undefined;
domain?: string | undefined;
secure?: boolean | undefined;
encode?: ((val: string) => string) | undefined;
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
}
}
}

Expand Down Expand Up @@ -317,4 +329,21 @@ export class PlatformResponse<Res extends Record<string, any> = any> {
isHeadersSent() {
return this.getRes().headersSent;
}

cookie(name: string, value: string | null, opts?: TsED.SetCookieOpts) {
if (value === null) {
this.raw.clearCookie(name);
return this;
}

this.raw.cookie(name, value, opts);

const cookie = this.raw.get("set-cookie");

if (!isArray(value)) {
this.raw.set("set-cookie", [].concat(cookie));
}

return this;
}
}
5 changes: 1 addition & 4 deletions packages/platform/platform-express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@
"@tsed/json-mapper": {
"optional": false
},
"@tsed/openspec": {
"optional": false
},
"@tsed/platform-views": {
"optional": true
},
Expand All @@ -123,4 +120,4 @@
"optional": false
}
}
}
}

0 comments on commit 71b5fe5

Please sign in to comment.