Skip to content

Commit

Permalink
Merge pull request #1 from AP-Atul/feat/multiple-options
Browse files Browse the repository at this point in the history
Feat/multiple options
  • Loading branch information
ap-atul authored Aug 20, 2022
2 parents 168a00f + 620fbb6 commit 5421c07
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 186 deletions.
67 changes: 52 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ using with AWS S3 sign urls for private objects.
npm i hapi-signed-url
```

## Route Options

| Key | Type | Description |
| ------------ | -------------------------------- | --------------------------------------------------------------------------- |
| lenses | Lens<object, string>[] | Array of lenses, this should be `R.lensProp<string, string>(key)` |
| pathToSource | Lens<object, object \| object[]> | Path to the nested object, this should be `R.lensPath(['somepath', '...'])` |

## Basic Usage

- Import the plugin
Expand Down Expand Up @@ -43,7 +50,7 @@ await server.register([
}
```

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

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

- Final response

```json
```js
{
"file": "random_id_SIGNATURE", // this value will be updated
"file": "random_id_SIGNATURE", // this value is updated
"name": "this is a file"
}
```
Expand Down Expand Up @@ -119,24 +126,54 @@ server.route({
});
```

### Note

- It will work with single objects and arrays. `pathToSource` is optional field,
use when nested objects are to be updated.
## For multiple nested keys

- Improvements todo
- Change the options structure to following, which will allow using multiple paths
Example with multiple options

```js
const options = {
sources: [
```ts
const responseObject = {
name: 'atul',
profile: '1212121', // to sign
projects: [
{
lenses: [nameLens],
id: '1',
files: '1234', // to sign
},
{
lenses: [fileLens],
path: docLens,
id: '2',
files: '123232', // to sign
},
],
};

// lenses for the entire object
const profileLens = R.lensProp<string, string>('profile');
const filesLens = R.lensProp<string, string>('files');

// path for nested object
const projectPath = R.lensPath(['projects']);

// server route config
server.route({
method: 'GET',
path: '/sample',
options: {
handler: handler.performAction,
plugins: {
signedUrl: [
// for profile sign
{
lenses: [profileLens],
},

// for files signing
{
lenses: [fileLens],
pathToSource: projectPath,
}
]
},
...
},
});
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hapi-signed-url",
"version": "1.0.2",
"version": "1.0.3",
"description": "A hapijs plugin to generate signed url for response objects",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down
89 changes: 56 additions & 33 deletions src/lib/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isEmpty, isNil, view, set, type } from 'ramda';
import { RouteOptions } from './types';
import { isEmpty, isNil, set, view, type } from 'ramda';
import { PluginOptions, Response, RouteOptions } from './types';

const isPluginActive = (request): boolean => {
return (
Expand All @@ -14,67 +14,90 @@ const isResponseAbsent = (request): boolean => {
);
};

const getRouteOptions = (request, getSignedUrl): RouteOptions => {
const validateRouteOptions = (options: RouteOptions | RouteOptions[]): void => {
if (Array.isArray(options)) {
options.map((option) => validateRouteOptions(option));
} else if (!options.lenses) {
throw new Error('hapi-signed-url: requires lenses in route options');
}
};

const validatePluginOptions = (options: PluginOptions): void => {
if (type(options.getSignedUrl) !== 'Function') {
throw new Error('hapi-signed-url: requires getSignedUrl function while registering');
}
};

const getRouteOptions = (request): RouteOptions[] => {
const options = request.route.settings.plugins.signedUrl;
const source = options.pathToSource
? view(options.pathToSource, request.response.source)
: request.response.source;
return {
...options,
source: source,
getSignedUrl: getSignedUrl,
};
return Array.isArray(options) ? options : [options];
};

const updateSignedUrl = async (options: RouteOptions) => {
const { source, lenses, getSignedUrl } = options;
const toUpdateLinks: string[] = lenses.map((lens) => view(lens, source));
const promises = toUpdateLinks.map(async (link: string) => await getSignedUrl(link));
const updateSignedUrl = async (
source: object,
routeOptions: RouteOptions,
pluginOptions: PluginOptions,
): Promise<object> => {
const { lenses } = routeOptions;
const toUpdateLinks = lenses.map((lens) => view(lens, source));
const promises = toUpdateLinks.map(
async (link) => await pluginOptions.getSignedUrl(link),
);
const updatedLinks = await Promise.all(promises);
const updatedSource = updatedLinks.reduce((source, link, index) => {
return view(lenses[index], source) ? set(lenses[index], link, source) : source;
}, source);
return updatedSource as object;
return updatedSource;
};

const processSource = async (options: RouteOptions) => {
const processSource = async (
source: object | object[],
routeOptions: RouteOptions,
pluginOptions: PluginOptions,
): Promise<object | object[]> => {
// single object
if (type(options.source) !== 'Array') {
return updateSignedUrl(options);
if (!Array.isArray(source)) {
return updateSignedUrl(source, routeOptions, pluginOptions);
}

// if source is array
const promises = (options.source as any[]).map(async (src) => {
return updateSignedUrl({
...options,
source: src,
});
const promises = source.map(async (src) => {
return updateSignedUrl(src, routeOptions, pluginOptions);
});
return Promise.all(promises);
};

const signUrl = (getSignedUrl) => async (request, h) => {
const signUrl = (options: PluginOptions) => async (request, h) => {
if (!isPluginActive(request)) {
return h.continue;
}
if (isResponseAbsent(request)) {
return h.continue;
}

const routeOptions = getRouteOptions(request, getSignedUrl);
const updated = await processSource(routeOptions);
const updatedSource = routeOptions.pathToSource
? set(routeOptions.pathToSource, updated, request.response.source)
: updated;
const routeOptions = getRouteOptions(request);
validateRouteOptions(routeOptions);
let toUpdateResponse = request.response.source as Response;

for (const routeOption of routeOptions) {
const source = routeOption.pathToSource
? view(routeOption.pathToSource, toUpdateResponse)
: toUpdateResponse;
const processed = await processSource(source, routeOption, options);
toUpdateResponse = routeOption.pathToSource
? set(routeOption.pathToSource, processed, toUpdateResponse)
: processed;
}

request.response.source = updatedSource;
request.response.source = toUpdateResponse;
return h.continue;
};

export const signedUrl = {
name: 'signedUrl',
version: '1.0.0',
register: async function (server, options) {
server.ext('onPreResponse', signUrl(options.getSignedUrl));
register: (server, options: PluginOptions) => {
validatePluginOptions(options);
server.ext('onPreResponse', signUrl(options));
},
};
13 changes: 9 additions & 4 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Lens } from 'ramda';

export interface RouteOptions {
// lenses for properties to update
lenses: any[];
lenses: Lens<object, string>[];
// path from which source to extract
pathToSource: any;
// source object can also be array
source: object | any[];
pathToSource?: Lens<object, Response>;
}

export interface PluginOptions {
// function to generate signed urls
getSignedUrl(key: string): Promise<string>;
}

export type Response = object | object[];
36 changes: 36 additions & 0 deletions test/env/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as R from 'ramda';

const generateRandomString = (): string =>
(Math.random() + 1).toString(36).substring(7).toString();

export const getSignedUrl = async (key: string) => (key ? `SIGNED_${key}` : '');

export const getSampleObject = async (): Promise<[any, object, object]> => {
const data = {
type: 'png',
image: generateRandomString(),
};
return [
R.lensProp<string, any>('image'),
data,
{
...data,
image: await getSignedUrl(data.image),
},
];
};

export const getAnotherObject = async (): Promise<[any, object, object]> => {
const data = {
mime: 'png',
file: generateRandomString(),
};
return [
R.lensProp<string, any>('file'),
data,
{
...data,
file: await getSignedUrl(data.file),
},
];
};
54 changes: 8 additions & 46 deletions test/env/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as hapi from '@hapi/hapi';
import { signedUrl } from '../../src/index';
import { RouteOptions } from '../../src/lib/types';
import { fileSignFunction } from './types';
import * as R from 'ramda';

export const init = async (getSignedUrl: fileSignFunction) => {
export const init = async (
getSignedUrl: fileSignFunction,
options: RouteOptions | RouteOptions[],
) => {
const PORT = 4000;
const server = hapi.server({
port: PORT,
Expand All @@ -20,60 +23,19 @@ export const init = async (getSignedUrl: fileSignFunction) => {
},
]);

// defining custom lenses
const fileLens = R.lensProp<string, any>('file');
const imageLens = R.lensProp<string, any>('image');

const nestedLevelOnePath = R.lensPath(['data']);
const nestedLevelTwoPath = R.lensPath(['data', 'images']);

server.route({
method: 'POST',
path: '/lenses',
options: {
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
return h.response(request.payload).code(200);
},
plugins: {
signedUrl: {
lenses: [fileLens, imageLens],
},
},
},
});

// creating a sample route to test
server.route({
method: 'POST',
path: '/nested/level-one',
path: '/test',
options: {
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
return h.response(request.payload).code(200);
},
plugins: {
signedUrl: {
lenses: [fileLens, imageLens],
pathToSource: nestedLevelOnePath,
},
signedUrl: options,
},
},
});

server.route({
method: 'POST',
path: '/nested/level-two',
options: {
handler: (request: hapi.Request, h: hapi.ResponseToolkit) => {
return h.response(request.payload).code(200);
},
plugins: {
signedUrl: {
lenses: [fileLens, imageLens],
pathToSource: nestedLevelTwoPath,
},
},
},
});

await server.initialize();
return server;
};
Expand Down
Loading

0 comments on commit 5421c07

Please sign in to comment.