Skip to content

Commit 5421c07

Browse files
authored
Merge pull request #1 from AP-Atul/feat/multiple-options
Feat/multiple options
2 parents 168a00f + 620fbb6 commit 5421c07

File tree

7 files changed

+298
-186
lines changed

7 files changed

+298
-186
lines changed

README.md

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ using with AWS S3 sign urls for private objects.
1313
npm i hapi-signed-url
1414
```
1515

16+
## Route Options
17+
18+
| Key | Type | Description |
19+
| ------------ | -------------------------------- | --------------------------------------------------------------------------- |
20+
| lenses | Lens<object, string>[] | Array of lenses, this should be `R.lensProp<string, string>(key)` |
21+
| pathToSource | Lens<object, object \| object[]> | Path to the nested object, this should be `R.lensPath(['somepath', '...'])` |
22+
1623
## Basic Usage
1724

1825
- Import the plugin
@@ -43,7 +50,7 @@ await server.register([
4350
}
4451
```
4552

46-
- Create a lens using ramda for the above object
53+
- Create a lens using ramda for the above object. Ramda [lenses](!https://ramdajs.com/docs/#lensProp)
4754

4855
```js
4956
const lens = R.lensProp<string, any>('file') // here file is the key from object
@@ -71,9 +78,9 @@ server.route({
7178

7279
- Final response
7380

74-
```json
81+
```js
7582
{
76-
"file": "random_id_SIGNATURE", // this value will be updated
83+
"file": "random_id_SIGNATURE", // this value is updated
7784
"name": "this is a file"
7885
}
7986
```
@@ -119,24 +126,54 @@ server.route({
119126
});
120127
```
121128

122-
### Note
123-
124-
- It will work with single objects and arrays. `pathToSource` is optional field,
125-
use when nested objects are to be updated.
129+
## For multiple nested keys
126130

127-
- Improvements todo
128-
- Change the options structure to following, which will allow using multiple paths
131+
Example with multiple options
129132

130-
```js
131-
const options = {
132-
sources: [
133+
```ts
134+
const responseObject = {
135+
name: 'atul',
136+
profile: '1212121', // to sign
137+
projects: [
133138
{
134-
lenses: [nameLens],
139+
id: '1',
140+
files: '1234', // to sign
135141
},
136142
{
137-
lenses: [fileLens],
138-
path: docLens,
143+
id: '2',
144+
files: '123232', // to sign
139145
},
140146
],
141147
};
148+
149+
// lenses for the entire object
150+
const profileLens = R.lensProp<string, string>('profile');
151+
const filesLens = R.lensProp<string, string>('files');
152+
153+
// path for nested object
154+
const projectPath = R.lensPath(['projects']);
155+
156+
// server route config
157+
server.route({
158+
method: 'GET',
159+
path: '/sample',
160+
options: {
161+
handler: handler.performAction,
162+
plugins: {
163+
signedUrl: [
164+
// for profile sign
165+
{
166+
lenses: [profileLens],
167+
},
168+
169+
// for files signing
170+
{
171+
lenses: [fileLens],
172+
pathToSource: projectPath,
173+
}
174+
]
175+
},
176+
...
177+
},
178+
});
142179
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hapi-signed-url",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "A hapijs plugin to generate signed url for response objects",
55
"main": "dist/src/index.js",
66
"types": "dist/src/index.d.ts",

src/lib/plugin.ts

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { isEmpty, isNil, view, set, type } from 'ramda';
2-
import { RouteOptions } from './types';
1+
import { isEmpty, isNil, set, view, type } from 'ramda';
2+
import { PluginOptions, Response, RouteOptions } from './types';
33

44
const isPluginActive = (request): boolean => {
55
return (
@@ -14,67 +14,90 @@ const isResponseAbsent = (request): boolean => {
1414
);
1515
};
1616

17-
const getRouteOptions = (request, getSignedUrl): RouteOptions => {
17+
const validateRouteOptions = (options: RouteOptions | RouteOptions[]): void => {
18+
if (Array.isArray(options)) {
19+
options.map((option) => validateRouteOptions(option));
20+
} else if (!options.lenses) {
21+
throw new Error('hapi-signed-url: requires lenses in route options');
22+
}
23+
};
24+
25+
const validatePluginOptions = (options: PluginOptions): void => {
26+
if (type(options.getSignedUrl) !== 'Function') {
27+
throw new Error('hapi-signed-url: requires getSignedUrl function while registering');
28+
}
29+
};
30+
31+
const getRouteOptions = (request): RouteOptions[] => {
1832
const options = request.route.settings.plugins.signedUrl;
19-
const source = options.pathToSource
20-
? view(options.pathToSource, request.response.source)
21-
: request.response.source;
22-
return {
23-
...options,
24-
source: source,
25-
getSignedUrl: getSignedUrl,
26-
};
33+
return Array.isArray(options) ? options : [options];
2734
};
2835

29-
const updateSignedUrl = async (options: RouteOptions) => {
30-
const { source, lenses, getSignedUrl } = options;
31-
const toUpdateLinks: string[] = lenses.map((lens) => view(lens, source));
32-
const promises = toUpdateLinks.map(async (link: string) => await getSignedUrl(link));
36+
const updateSignedUrl = async (
37+
source: object,
38+
routeOptions: RouteOptions,
39+
pluginOptions: PluginOptions,
40+
): Promise<object> => {
41+
const { lenses } = routeOptions;
42+
const toUpdateLinks = lenses.map((lens) => view(lens, source));
43+
const promises = toUpdateLinks.map(
44+
async (link) => await pluginOptions.getSignedUrl(link),
45+
);
3346
const updatedLinks = await Promise.all(promises);
3447
const updatedSource = updatedLinks.reduce((source, link, index) => {
3548
return view(lenses[index], source) ? set(lenses[index], link, source) : source;
3649
}, source);
37-
return updatedSource as object;
50+
return updatedSource;
3851
};
3952

40-
const processSource = async (options: RouteOptions) => {
53+
const processSource = async (
54+
source: object | object[],
55+
routeOptions: RouteOptions,
56+
pluginOptions: PluginOptions,
57+
): Promise<object | object[]> => {
4158
// single object
42-
if (type(options.source) !== 'Array') {
43-
return updateSignedUrl(options);
59+
if (!Array.isArray(source)) {
60+
return updateSignedUrl(source, routeOptions, pluginOptions);
4461
}
4562

4663
// if source is array
47-
const promises = (options.source as any[]).map(async (src) => {
48-
return updateSignedUrl({
49-
...options,
50-
source: src,
51-
});
64+
const promises = source.map(async (src) => {
65+
return updateSignedUrl(src, routeOptions, pluginOptions);
5266
});
5367
return Promise.all(promises);
5468
};
5569

56-
const signUrl = (getSignedUrl) => async (request, h) => {
70+
const signUrl = (options: PluginOptions) => async (request, h) => {
5771
if (!isPluginActive(request)) {
5872
return h.continue;
5973
}
6074
if (isResponseAbsent(request)) {
6175
return h.continue;
6276
}
6377

64-
const routeOptions = getRouteOptions(request, getSignedUrl);
65-
const updated = await processSource(routeOptions);
66-
const updatedSource = routeOptions.pathToSource
67-
? set(routeOptions.pathToSource, updated, request.response.source)
68-
: updated;
78+
const routeOptions = getRouteOptions(request);
79+
validateRouteOptions(routeOptions);
80+
let toUpdateResponse = request.response.source as Response;
81+
82+
for (const routeOption of routeOptions) {
83+
const source = routeOption.pathToSource
84+
? view(routeOption.pathToSource, toUpdateResponse)
85+
: toUpdateResponse;
86+
const processed = await processSource(source, routeOption, options);
87+
toUpdateResponse = routeOption.pathToSource
88+
? set(routeOption.pathToSource, processed, toUpdateResponse)
89+
: processed;
90+
}
6991

70-
request.response.source = updatedSource;
92+
request.response.source = toUpdateResponse;
7193
return h.continue;
7294
};
7395

7496
export const signedUrl = {
7597
name: 'signedUrl',
7698
version: '1.0.0',
77-
register: async function (server, options) {
78-
server.ext('onPreResponse', signUrl(options.getSignedUrl));
99+
register: (server, options: PluginOptions) => {
100+
validatePluginOptions(options);
101+
server.ext('onPreResponse', signUrl(options));
79102
},
80103
};

src/lib/types.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import { Lens } from 'ramda';
2+
13
export interface RouteOptions {
24
// lenses for properties to update
3-
lenses: any[];
5+
lenses: Lens<object, string>[];
46
// path from which source to extract
5-
pathToSource: any;
6-
// source object can also be array
7-
source: object | any[];
7+
pathToSource?: Lens<object, Response>;
8+
}
9+
10+
export interface PluginOptions {
811
// function to generate signed urls
912
getSignedUrl(key: string): Promise<string>;
1013
}
14+
15+
export type Response = object | object[];

test/env/factory.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as R from 'ramda';
2+
3+
const generateRandomString = (): string =>
4+
(Math.random() + 1).toString(36).substring(7).toString();
5+
6+
export const getSignedUrl = async (key: string) => (key ? `SIGNED_${key}` : '');
7+
8+
export const getSampleObject = async (): Promise<[any, object, object]> => {
9+
const data = {
10+
type: 'png',
11+
image: generateRandomString(),
12+
};
13+
return [
14+
R.lensProp<string, any>('image'),
15+
data,
16+
{
17+
...data,
18+
image: await getSignedUrl(data.image),
19+
},
20+
];
21+
};
22+
23+
export const getAnotherObject = async (): Promise<[any, object, object]> => {
24+
const data = {
25+
mime: 'png',
26+
file: generateRandomString(),
27+
};
28+
return [
29+
R.lensProp<string, any>('file'),
30+
data,
31+
{
32+
...data,
33+
file: await getSignedUrl(data.file),
34+
},
35+
];
36+
};

test/env/server.ts

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as hapi from '@hapi/hapi';
22
import { signedUrl } from '../../src/index';
3+
import { RouteOptions } from '../../src/lib/types';
34
import { fileSignFunction } from './types';
4-
import * as R from 'ramda';
55

6-
export const init = async (getSignedUrl: fileSignFunction) => {
6+
export const init = async (
7+
getSignedUrl: fileSignFunction,
8+
options: RouteOptions | RouteOptions[],
9+
) => {
710
const PORT = 4000;
811
const server = hapi.server({
912
port: PORT,
@@ -20,60 +23,19 @@ export const init = async (getSignedUrl: fileSignFunction) => {
2023
},
2124
]);
2225

23-
// defining custom lenses
24-
const fileLens = R.lensProp<string, any>('file');
25-
const imageLens = R.lensProp<string, any>('image');
26-
27-
const nestedLevelOnePath = R.lensPath(['data']);
28-
const nestedLevelTwoPath = R.lensPath(['data', 'images']);
29-
30-
server.route({
31-
method: 'POST',
32-
path: '/lenses',
33-
options: {
34-
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
35-
return h.response(request.payload).code(200);
36-
},
37-
plugins: {
38-
signedUrl: {
39-
lenses: [fileLens, imageLens],
40-
},
41-
},
42-
},
43-
});
44-
26+
// creating a sample route to test
4527
server.route({
4628
method: 'POST',
47-
path: '/nested/level-one',
29+
path: '/test',
4830
options: {
4931
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
5032
return h.response(request.payload).code(200);
5133
},
5234
plugins: {
53-
signedUrl: {
54-
lenses: [fileLens, imageLens],
55-
pathToSource: nestedLevelOnePath,
56-
},
35+
signedUrl: options,
5736
},
5837
},
5938
});
60-
61-
server.route({
62-
method: 'POST',
63-
path: '/nested/level-two',
64-
options: {
65-
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
66-
return h.response(request.payload).code(200);
67-
},
68-
plugins: {
69-
signedUrl: {
70-
lenses: [fileLens, imageLens],
71-
pathToSource: nestedLevelTwoPath,
72-
},
73-
},
74-
},
75-
});
76-
7739
await server.initialize();
7840
return server;
7941
};

0 commit comments

Comments
 (0)