Skip to content

Commit

Permalink
feat(action result builder): allow user to set SA response headers (#945
Browse files Browse the repository at this point in the history
)
  • Loading branch information
arnaud-moncel committed Feb 2, 2024
1 parent 2887878 commit ce37c01
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 6 deletions.
4 changes: 4 additions & 0 deletions packages/agent/src/routes/modification/action/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export default class ActionRoute extends CollectionRoute {
const data = ForestValueConverter.makeFormData(dataSource, rawData, fields);
const result = await this.collection.execute(caller, this.actionName, data, filterForCaller);

if (result.responseHeaders) {
context.response.set(result.responseHeaders);
}

if (result?.type === 'Error') {
context.response.status = HttpCode.BadRequest;
context.response.body = { error: result.message, html: result.html };
Expand Down
25 changes: 25 additions & 0 deletions packages/agent/test/routes/modification/action/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,31 @@ describe('ActionRoute', () => {
});
});

test('it should handle response headers', async () => {
dataSource = factories.dataSource.buildWithCollections([
factories.collection.build({
name: 'books',
schema: { actions: { MySingleAction: { scope: 'Global' } } },
getForm: jest.fn(),
execute: jest.fn().mockResolvedValue({
type: 'Error',
message: 'the result does not matter',
responseHeaders: { test: 'test' },
}),
}),
]);

route = new ActionRoute(services, options, dataSource, 'books', 'MySingleAction');

// Test
const context = createMockContext(baseContext);

// @ts-expect-error: test private method
await route.handleExecute(context);

expect(context.response.headers).toHaveProperty('test', 'test');
});

describe('with a single action used from list-view, detail-view & summary', () => {
beforeEach(() => {
dataSource = factories.dataSource.buildWithCollections([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { ActionResult } from '@forestadmin/datasource-toolkit';
import { ActionHeaders, ActionResult } from '@forestadmin/datasource-toolkit';
import { Readable } from 'stream';

export default class ResultBuilder {
private responseHeaders: ActionHeaders = {};

/**
* Add header to the action response
* @param name the header name
* @param value the header value
* @example
* .setHeader('myHeaderName', 'my header value');
*/
setHeader(name: string, value: string) {
this.responseHeaders[name] = value;

return this;
}

/**
* Returns a success response from the action
* @param message the success message to return
Expand All @@ -15,6 +30,7 @@ export default class ResultBuilder {
message: message ?? 'Success',
invalidated: new Set(options?.invalidated ?? []),
html: options?.html,
responseHeaders: this.responseHeaders,
};
}

Expand All @@ -30,6 +46,7 @@ export default class ResultBuilder {
type: 'Error',
message: message ?? 'Error',
html: options?.html,
responseHeaders: this.responseHeaders,
};
}

Expand All @@ -54,6 +71,7 @@ export default class ResultBuilder {
method,
headers,
body,
responseHeaders: this.responseHeaders,
};
}

Expand All @@ -78,6 +96,7 @@ export default class ResultBuilder {
streamOrBufferOrString instanceof Readable
? streamOrBufferOrString
: Readable.from([streamOrBufferOrString]),
responseHeaders: this.responseHeaders,
};
}

Expand All @@ -88,6 +107,6 @@ export default class ResultBuilder {
* .redirectTo('https://www.google.com');
*/
redirectTo(path: string): ActionResult {
return { type: 'Redirect', path };
return { type: 'Redirect', path, responseHeaders: this.responseHeaders };
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,88 @@
import ResultBuilder from '../../../src/decorators/actions/result-builder';

describe('ResultBuilder', () => {
const builder = new ResultBuilder();
let builder: ResultBuilder;

beforeEach(() => {
builder = new ResultBuilder();
});

test('success', () => {
expect(builder.success('Great!')).toEqual({
type: 'Success',
message: 'Great!',
invalidated: new Set(),
responseHeaders: {},
});

expect(builder.success('Great!', { html: '<div>That worked!</div>' })).toEqual({
type: 'Success',
message: 'Great!',
html: '<div>That worked!</div>',
invalidated: new Set(),
responseHeaders: {},
});

expect(builder.setHeader('test', 'test').success('Great!')).toEqual({
type: 'Success',
message: 'Great!',
invalidated: new Set(),
responseHeaders: { test: 'test' },
});
});

test('error', () => {
expect(builder.error('booo')).toEqual({
type: 'Error',
message: 'booo',
responseHeaders: {},
});

expect(builder.error('booo', { html: '<div>html content</div>' })).toEqual({
type: 'Error',
message: 'booo',
html: '<div>html content</div>',
responseHeaders: {},
});

expect(builder.setHeader('test', 'test').error('booo')).toEqual({
type: 'Error',
message: 'booo',
responseHeaders: { test: 'test' },
});
});

test('file', () => {
const result = builder.file('col1,col2,col3', 'test.csv', 'text/csv');
let result = builder.file('col1,col2,col3', 'test.csv', 'text/csv');

expect(result).toMatchObject({
type: 'File',
name: 'test.csv',
mimeType: 'text/csv',
responseHeaders: {},
});

result = builder.setHeader('test', 'test').file('col1,col2,col3', 'test.csv', 'text/csv');

expect(result).toMatchObject({
type: 'File',
name: 'test.csv',
mimeType: 'text/csv',
responseHeaders: { test: 'test' },
});
});

test('redirect', () => {
expect(builder.redirectTo('/mypath')).toEqual({
type: 'Redirect',
path: '/mypath',
responseHeaders: {},
});

expect(builder.setHeader('test', 'test').redirectTo('/mypath')).toEqual({
type: 'Redirect',
path: '/mypath',
responseHeaders: { test: 'test' },
});
});

Expand All @@ -54,6 +93,16 @@ describe('ResultBuilder', () => {
method: 'POST',
headers: {},
body: {},
responseHeaders: {},
});

expect(builder.setHeader('test', 'test').webhook('http://someurl')).toEqual({
type: 'Webhook',
url: 'http://someurl',
method: 'POST',
headers: {},
body: {},
responseHeaders: { test: 'test' },
});
});
});
7 changes: 5 additions & 2 deletions packages/datasource-toolkit/src/interfaces/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,12 @@ export type RedirectResult = {
path: string;
};

export type ActionResult =
export type ActionHeaders = { [headerName: string]: string };

export type ActionResult = { responseHeaders?: ActionHeaders } & (
| SuccessResult
| ErrorResult
| WebHookResult
| FileResult
| RedirectResult;
| RedirectResult
);

0 comments on commit ce37c01

Please sign in to comment.