Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(action result builder): allow user to set SA response headers #945

Merged
merged 6 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪

}

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
@@ -1,7 +1,7 @@
import { Readable } from 'stream';

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any

Check warning on line 4 in packages/datasource-toolkit/src/interfaces/action.ts

View workflow job for this annotation

GitHub Actions / Linting & Testing (datasource-toolkit)

Unexpected any. Specify a different type
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>>
: never;
// This is a trick to disallow properties
Expand Down Expand Up @@ -283,9 +283,12 @@
path: string;
};

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

export type ActionResult = { responseHeaders?: ActionHeaders } & (
| SuccessResult
| ErrorResult
| WebHookResult
| FileResult
| RedirectResult;
| RedirectResult
);
103 changes: 0 additions & 103 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1362,109 +1362,6 @@
path-to-regexp "^6.1.0"
reusify "^1.0.4"

"@forestadmin/agent@1.36.11":
version "1.36.11"
resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.36.11.tgz#17b830f901b17a47d183906f721c11232fcf10b5"
integrity sha512-WB1L5EpreVzjR2YhlxcRAP2TAUzwy79pdeffTHzueAkTUf0r0yRFUn+0eK6QMbkNqdmJbxNqZlCtxZo9wRrPTg==
dependencies:
"@fast-csv/format" "^4.3.5"
"@fastify/express" "^1.1.0"
"@forestadmin/datasource-customizer" "1.39.3"
"@forestadmin/datasource-toolkit" "1.29.1"
"@forestadmin/forestadmin-client" "1.25.3"
"@koa/cors" "^5.0.0"
"@koa/router" "^12.0.0"
forest-ip-utils "^1.0.1"
json-api-serializer "^2.6.6"
json-stringify-pretty-compact "^3.0.0"
jsonwebtoken "^9.0.0"
koa "^2.14.1"
koa-bodyparser "^4.3.0"
koa-jwt "^4.0.4"
luxon "^3.2.1"
object-hash "^3.0.0"
superagent "^8.0.6"
uuid "^9.0.0"

"@forestadmin/datasource-customizer@1.39.3":
version "1.39.3"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.39.3.tgz#10365e79d3100f5a2e347d8cc39e3b98f8bc2856"
integrity sha512-gjFMqCSWU8uPYSETsC22Dkk3kbk9VV7V1FSNjhipkPyjO+duoM9IafuOb5AwJa8rFhF6y853xngXL8WBr71ugw==
dependencies:
"@forestadmin/datasource-toolkit" "1.29.1"
file-type "^16.5.4"
luxon "^3.2.1"
object-hash "^3.0.0"
uuid "^9.0.0"

"@forestadmin/datasource-customizer@1.40.0":
version "1.40.0"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-customizer/-/datasource-customizer-1.40.0.tgz#b7ab00c3fc13441567f1ff1924f9f147f6160517"
integrity sha512-WEKv6wIlYvmMq2CEQRiwxfPbLf99NAAZfpBFLct8Vjt3ofYydw+qEsjbRZqB9RruIdeF2I1tg6Xgr00pr27jYg==
dependencies:
"@forestadmin/datasource-toolkit" "1.29.1"
file-type "^16.5.4"
luxon "^3.2.1"
object-hash "^3.0.0"
uuid "^9.0.0"

"@forestadmin/datasource-dummy@1.0.90":
version "1.0.90"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-dummy/-/datasource-dummy-1.0.90.tgz#a4310f1417808a7f2c3d60452478cb54b352ac5e"
integrity sha512-n2/ls1/ms5qxTQst4HaCgh9Ol4po9oWQtdSxOPoFu0vOVywmy43GyP28OY4+Wir4oMmGXqXugd96R+XulUC3uA==
dependencies:
"@forestadmin/datasource-customizer" "1.40.0"
"@forestadmin/datasource-toolkit" "1.29.1"

"@forestadmin/datasource-mongoose@1.5.32":
version "1.5.32"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-mongoose/-/datasource-mongoose-1.5.32.tgz#dfdad4dd0d63f93ba256d1136f990c334e3b3007"
integrity sha512-qkgYlr9+wQc+O9gXrHaQ6gI29SOlFWvp1CsEZ/wkjeH0k5GRQXSnkRkiDL4lVH1xb6Lk6OeVKdQX9la6sHef/g==
dependencies:
"@forestadmin/datasource-toolkit" "1.29.1"
luxon "^3.2.1"

"@forestadmin/datasource-sequelize@1.5.26":
version "1.5.26"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sequelize/-/datasource-sequelize-1.5.26.tgz#061b59d6c048c0642f8fd267d3f297a6abf8058d"
integrity sha512-wWaDi49NIJVxb2oglV/mIDK895Gi3A13o1ndt+9wuWMERlYQgOUEbySS5RntONzSsJ2OfdDsExIcKLmF6Btnrg==
dependencies:
"@forestadmin/datasource-toolkit" "1.29.1"

"@forestadmin/datasource-sql@1.7.43":
version "1.7.43"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-sql/-/datasource-sql-1.7.43.tgz#a6e2b7f90d2cb8a547888c48736a6322e8245772"
integrity sha512-Z0U5OTBtL6QWJgTLhzK9AurBhVqg/ZzRTMCLA/bOtV9zRuObz4WiqAT6O6L0og0KEtzYtmMioUfAhoOHYSIy0A==
dependencies:
"@forestadmin/datasource-sequelize" "1.5.26"
"@forestadmin/datasource-toolkit" "1.29.1"
"@types/ssh2" "^1.11.11"
pluralize "^8.0.0"
sequelize "^6.28.0"
socks "^2.7.1"
ssh2 "^1.14.0"

"@forestadmin/datasource-toolkit@1.29.1":
version "1.29.1"
resolved "https://registry.yarnpkg.com/@forestadmin/datasource-toolkit/-/datasource-toolkit-1.29.1.tgz#bf1cda9d3684208509a891367b39ec7ff112618c"
integrity sha512-JJnxRJyvZUIemp+mU0sZwIieiJys2RAAMb8JPNqmSWDmkXgVV0t0mbdmegF3xwk8lQvYbRqcTNOrT9Q0wjLXoQ==
dependencies:
luxon "^3.2.1"
object-hash "^3.0.0"
uuid "^9.0.0"

"@forestadmin/forestadmin-client@1.25.3":
version "1.25.3"
resolved "https://registry.yarnpkg.com/@forestadmin/forestadmin-client/-/forestadmin-client-1.25.3.tgz#1924a8c3e52e18282d633c2aa92137519ca9ce30"
integrity sha512-c/0igkHStVem+7SVgQGmiPR3HR6WGjQBknFdBlHUypb7qADdBeMh81meMsaq5rT74czafdb5u4VWUDu29i7f5Q==
dependencies:
eventsource "2.0.2"
json-api-serializer "^2.6.6"
jsonwebtoken "^9.0.0"
object-hash "^3.0.0"
openid-client "^5.3.1"
superagent "^8.0.6"

"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
Expand Down
Loading