Skip to content

Commit 7a8c8f0

Browse files
Merge pull request #1438 from opencomponents/set-cookie
feat: add setCookie functionality to component context
2 parents d3a8aa4 + 3064344 commit 7a8c8f0

File tree

12 files changed

+171
-2
lines changed

12 files changed

+171
-2
lines changed

src/registry/routes/component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Readable } from 'node:stream';
22
import { encode } from '@rdevis/turbo-stream';
3-
import type { Request, RequestHandler, Response } from 'express';
3+
import type { CookieOptions, Request, RequestHandler, Response } from 'express';
44
import { serializeError } from 'serialize-error';
55
import strings from '../../resources';
66
import type { Config } from '../../types';
@@ -49,6 +49,14 @@ export default function component(
4949
res.set(result.headers);
5050
}
5151

52+
if (Array.isArray(result.cookies) && result.cookies.length > 0) {
53+
for (const cookie of result.cookies) {
54+
const opts: CookieOptions = (cookie.options ??
55+
{}) as CookieOptions;
56+
res.cookie(cookie.name, cookie.value as any, opts);
57+
}
58+
}
59+
5260
const streamEnabled =
5361
!!result.response.data?.component?.props?.[stream];
5462
if (streamEnabled) {

src/registry/routes/components.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import async from 'async';
22

3-
import type { Request, RequestHandler, Response } from 'express';
3+
import type { CookieOptions, Request, RequestHandler, Response } from 'express';
44
import strings from '../../resources';
55
import type { Config } from '../../types';
66
import type { Repository } from '../domain/repository';
@@ -30,6 +30,23 @@ export default function components(
3030
res.set(results[0].headers);
3131
};
3232

33+
const setCookies = (
34+
results: GetComponentResult[] | undefined,
35+
res: Response
36+
) => {
37+
if (!results || results.length !== 1 || !results[0] || !res.cookie) {
38+
return;
39+
}
40+
const cookies = results[0].cookies;
41+
if (Array.isArray(cookies) && cookies.length > 0) {
42+
for (const cookie of cookies) {
43+
const opts: CookieOptions = (cookie.options ?? {}) as CookieOptions;
44+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
45+
res.cookie(cookie.name, cookie.value as any, opts);
46+
}
47+
}
48+
};
49+
3350
return (req: Request, res: Response) => {
3451
const components = req.body.components as Component[];
3552
const registryErrors = strings.errors.registry;
@@ -85,6 +102,7 @@ export default function components(
85102
(_err: any, results: GetComponentResult[]) => {
86103
try {
87104
setHeaders(results, res);
105+
setCookies(results, res);
88106
res.status(200).json(results);
89107
} catch (e) {
90108
// @ts-ignore I think this will never reach (how can setHeaders throw?)

src/registry/routes/helpers/get-component.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Domain from 'node:domain';
33
import type { IncomingHttpHeaders } from 'node:http';
44
import vm from 'node:vm';
55
import acceptLanguageParser from 'accept-language-parser';
6+
import type { CookieOptions } from 'express';
67
import Cache from 'nice-cache';
78
import Client from 'oc-client';
89
import emptyResponseHandler from 'oc-empty-response-handler';
@@ -43,6 +44,11 @@ export interface RendererOptions {
4344
export interface GetComponentResult {
4445
status: number;
4546
headers?: Record<string, string>;
47+
cookies?: Array<{
48+
name: string;
49+
value: any;
50+
options?: CookieOptions;
51+
}>;
4652
response: {
4753
data?: any;
4854
type?: string;
@@ -143,6 +149,11 @@ export default function getComponent(conf: Config, repository: Repository) {
143149
const nestedRenderer = NestedRenderer(renderer, options.conf);
144150
const retrievingInfo = GetComponentRetrievingInfo(options);
145151
let responseHeaders: Record<string, string> = {};
152+
const responseCookies: Array<{
153+
name: string;
154+
value: any;
155+
options?: CookieOptions;
156+
}> = [];
146157

147158
const getLanguage = () => {
148159
const paramOverride =
@@ -151,6 +162,9 @@ export default function getComponent(conf: Config, repository: Repository) {
151162
};
152163

153164
const callback = (result: GetComponentResult) => {
165+
if (responseCookies.length > 0 && !result.cookies) {
166+
result.cookies = responseCookies;
167+
}
154168
if (result.response.error) {
155169
retrievingInfo.extend(result.response);
156170
}
@@ -578,6 +592,17 @@ export default function getComponent(conf: Config, repository: Repository) {
578592
responseHeaders[header.toLowerCase()] = value;
579593
}
580594
},
595+
setCookie: (
596+
name?: string,
597+
value?: any,
598+
options?: CookieOptions
599+
) => {
600+
if (typeof name !== 'string') {
601+
throw strings.errors.registry
602+
.COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID;
603+
}
604+
responseCookies.push({ name, value, options });
605+
},
581606
templates: repository.getTemplatesInfo(),
582607
streamSymbol: stream
583608
};

src/resources/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export default {
9898
COMPONENT_VERSION_NOT_VALID_CODE: 'version_not_valid',
9999
COMPONENT_SET_HEADER_PARAMETERS_NOT_VALID:
100100
'context.setHeader parameters must be strings',
101+
COMPONENT_SET_COOKIE_PARAMETERS_NOT_VALID:
102+
'context.setCookie parameters are not valid',
101103
CONFIGURATION_DEPENDENCIES_MUST_BE_ARRAY:
102104
'Registry configuration is not valid: dependencies must be an array',
103105
CONFIGURATION_EMPTY: 'Registry configuration is empty',

test/acceptance/registry.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,27 @@ describe('registry', () => {
153153
});
154154
});
155155

156+
describe('GET /hello-world-custom-cookies', () => {
157+
describe('with the default configuration and strong version 1.0.0', () => {
158+
before((done) => {
159+
next(
160+
request('http://localhost:3030/hello-world-custom-cookies/1.0.0'),
161+
done
162+
);
163+
});
164+
165+
it('should return the component and set cookies', () => {
166+
expect(result.version).to.equal('1.0.0');
167+
expect(result.name).to.equal('hello-world-custom-cookies');
168+
expect(result.headers).to.be.undefined;
169+
const setCookie = headers['set-cookie'];
170+
expect(setCookie).to.be.an('array');
171+
expect(setCookie.join(';')).to.contain('Test-Cookie=Cookie-Value');
172+
expect(setCookie.join(';')).to.contain('Another-Cookie=Another-Value');
173+
});
174+
});
175+
});
176+
156177
describe('POST /hello-world-custom-headers', () => {
157178
describe('with the default configuration (no customHeadersToSkipOnWeakVersion defined) and strong version 1.0.0', () => {
158179
before((done) => {
@@ -351,6 +372,43 @@ describe('registry', () => {
351372
});
352373
});
353374

375+
describe('POST /hello-world-custom-cookies', () => {
376+
describe('with the default configuration and strong version 1.0.0', () => {
377+
before((done) => {
378+
next(
379+
request('http://localhost:3030', {
380+
method: 'POST',
381+
headers: {
382+
'Content-Type': 'application/json'
383+
},
384+
body: JSON.stringify({
385+
components: [
386+
{
387+
name: 'hello-world-custom-cookies',
388+
version: '1.0.0'
389+
}
390+
]
391+
})
392+
}),
393+
done
394+
);
395+
});
396+
397+
it('should set HTTP cookies', () => {
398+
const setCookie = headers['set-cookie'];
399+
expect(setCookie).to.be.an('array');
400+
expect(setCookie.join(';')).to.contain('Test-Cookie=Cookie-Value');
401+
expect(setCookie.join(';')).to.contain('Another-Cookie=Another-Value');
402+
});
403+
404+
it('should return the component and keep headers in body the same', () => {
405+
expect(result[0].response.version).to.equal('1.0.0');
406+
expect(result[0].response.name).to.equal('hello-world-custom-cookies');
407+
expect(result[0].headers).to.be.deep.equal({});
408+
});
409+
});
410+
});
411+
354412
describe('GET /', () => {
355413
before((done) => {
356414
next(request('http://localhost:3030'), done);
@@ -368,6 +426,7 @@ describe('registry', () => {
368426
'http://localhost:3030/empty',
369427
'http://localhost:3030/handlebars3-component',
370428
'http://localhost:3030/hello-world',
429+
'http://localhost:3030/hello-world-custom-cookies',
371430
'http://localhost:3030/hello-world-custom-headers',
372431
'http://localhost:3030/jade-filters',
373432
'http://localhost:3030/language',
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "hello-world-custom-cookies",
3+
"description": "",
4+
"version": "1.0.0",
5+
"repository": "",
6+
"oc": {
7+
"files": {
8+
"template": {
9+
"type": "handlebars",
10+
"hashKey": "dbf1f0cb2ae6a52f402b26dff36d5bd696519933",
11+
"src": "template.js",
12+
"version": "6.0.25",
13+
"size": 187
14+
},
15+
"dataProvider": {
16+
"type": "node.js",
17+
"hashKey": "ef8a15013fac7073b79cc9b21113f1d699ee83ae",
18+
"src": "server.js",
19+
"size": 334
20+
},
21+
"static": []
22+
},
23+
"version": "0.50.27",
24+
"packaged": true,
25+
"date": 1756543269656
26+
}
27+
}

test/fixtures/components/hello-world-custom-cookies/_package/server.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/components/hello-world-custom-cookies/_package/template.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "hello-world-custom-cookies",
3+
"description": "",
4+
"version": "1.0.0",
5+
"repository": "",
6+
"oc": {
7+
"files": {
8+
"data": "server.js",
9+
"template": {
10+
"src": "template.html",
11+
"type": "handlebars"
12+
}
13+
}
14+
}
15+
}
16+
17+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
module.exports.data = function (context, callback) {
4+
context.setCookie('Test-Cookie', 'Cookie-Value');
5+
context.setCookie('Another-Cookie', 'Another-Value', { httpOnly: true });
6+
callback(null, {});
7+
};
8+
9+

0 commit comments

Comments
 (0)