Skip to content
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ npm i @quickcase/node-toolkit
* [Cache](#cache)
* [Case](#case)
* [Case Access](#case-access)
* [Config](#config)
* [Definition](#definition)
* [Document](#document)
* [Express](#express)
Expand Down Expand Up @@ -434,6 +435,97 @@ const client = httpClient('http://data-store:4452')(() => Promise.resolve('acces
await revokeUserAccess(client)('1234123412341238')('user-1');
```

### Config

Utilities to deal with configuration objects.

#### camelConfig(config)

Recursively convert keys in configuration objects to camel case for consistency and ease of use.

##### Arguments

| Name | Type | Description |
|------|------|-------------|
| config | object | Required. Object containing the key/value pair of configuration properties. |

##### Returns

`object` with the same shape as `config` but for which all keys are now camel case. Values are preserved unaltered.

#### Example

```javascript
import {camelConfig} from '@quickcase/node-toolkit';

const config = camelConfig({
'a-prop-2': {
'a_prop_21': undefined,
'a-prop-22': 'override21',
},
});

/*
{
aProp2: {
aProp21: 'value21',
aProp22: 'override21',
},
}
*/
```

#### mergeConfig(defaultConfig)(overrides)

Deep merge of a default configuration with partial overrides.

##### Arguments

| Name | Type | Description |
|------|------|-------------|
| defaultConfig | object| Required. Object representing the entire configuration contract with default values for all properties. Properties which do not have a default value must be explicitly assigned `undefined`. |
| overrides | object | Required. Subset of `defaultConfig`. Overridden properties must exactly match the shape of `defaultConfig` |

##### Returns

`object` with the same shape as `defaultConfig` and containing the merged properties of `defaultConfig` and `overrides`.

#### Example

```javascript
import {mergeConfig} from '@quickcase/node-toolkit';

const DEFAULT_CONFIG = {
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'value21',
prop23: 'value23',
},
prop3: undefined,
};

const config = mergeConfig(DEFAULT_CONFIG)({
prop2: {
prop21: undefined,
prop22: 'override21',
prop23: null,
}
});

/*
{
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'override21',
prop23: null,
},
prop3: undefined,
}
*/
```

### Definition

#### fetchCaseType(httpClient)(caseTypeId)()
Expand Down
34 changes: 30 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"homepage": "https://github.com/quickcase/node-toolkit#readme",
"dependencies": {
"axios": "^0.21.1",
"camelcase": "^6.2.0",
"jsonschema": "^1.4.0",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.0.3",
"redis": "^3.0.2"
Expand Down
32 changes: 32 additions & 0 deletions src/modules/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import camelCase from 'camelcase';

export const mergeConfig = (defaultConfig) => (override = {}) => {
const mergedEntries = Object.entries(defaultConfig)
.map(([key, sourceValue]) => [key, sourceValue, override[key]])
.map(mergeConfigEntry);
return Object.fromEntries(mergedEntries);
};

const mergeConfigEntry = ([key, sourceValue, overrideValue]) => {
if (sourceValue && typeof sourceValue === 'object') {
if (overrideValue && typeof overrideValue === 'object') {
return [key, mergeConfig(sourceValue)(overrideValue)];
}
return [key, sourceValue];
}
return [key, overrideValue !== undefined ? overrideValue : sourceValue];
};

export const camelConfig = (config) => {
const entries = Object.entries(config)
.map(camelConfigEntry);
return Object.fromEntries(entries);
};

const camelConfigEntry = ([key, value]) => {
if (value && typeof value === 'object') {
return [camelCase(key), camelConfig(value)];
}

return [camelCase(key), value];
};
108 changes: 108 additions & 0 deletions src/modules/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {camelConfig, mergeConfig} from './config';

describe('mergeConfig', () => {
test('should return default configuration when no override provided', () => {
const config = mergeConfig({prop1: 'value1'})(undefined);
expect(config).toEqual({prop1: 'value1'});
});

test('should return default configuration when empty override provided', () => {
const config = mergeConfig({prop1: 'value1'})({});
expect(config).toEqual({prop1: 'value1'});
});

test('should override top-level properties', () => {
const config = mergeConfig({
prop1: 'value1',
prop2: 'value2',
})({
prop1: 'value3',
});
expect(config).toEqual({prop1: 'value3', prop2: 'value2'});
});

test('should override nested properties', () => {
const config = mergeConfig({
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'value22',
},
})({
prop2: {
prop22: 'value--'
}
});
expect(config).toEqual({
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'value--',
},
});
});

test('should ignore incorrect parent node overrides', () => {
const config = mergeConfig({
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'value22',
},
})({
prop2: 'incorrect type',
});
expect(config).toEqual({
prop1: 'value1',
prop2: {
prop21: 'value21',
prop22: 'value22',
},
});
});

test('should accept null overrides', () => {
const config = mergeConfig({
prop1: 'value1',
prop2: 'value2',
})({
prop2: null,
});
expect(config).toEqual({
prop1: 'value1',
prop2: null,
});
});
});

describe('camelConfig', () => {
test('should return config as is when already in camel case', () => {
const config = camelConfig({prop1: 'value1'});
expect(config).toEqual({prop1: 'value1'});
});

test('should camel case top-level properties', () => {
const config = camelConfig({
prop1: 'value1',
'a-prop-2': 'value2',
});
expect(config).toEqual({prop1: 'value1', aProp2: 'value2'});
});

test('should camel case nested properties', () => {
const config = camelConfig({
prop1: 'value1',
prop2: {
'a-nested-prop-2': 'value21',
'another_nested_prop_2': 'value22',
},
});
expect(config).toEqual({
prop1: 'value1',
prop2: {
aNestedProp2: 'value21',
anotherNestedProp2: 'value22',
},
});
});
});
2 changes: 2 additions & 0 deletions src/modules/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export * from './cache';
export * from './case';
export * from './case-access';
export * from './config';
export * from './definition';
export * from './document';
export * from './express';
export * from './field';
export * from './http-client';
export * from './oauth2';
export * from './oidc';
export * from './search';
export * from './redis-gateway';
15 changes: 15 additions & 0 deletions src/modules/oidc/access-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const simpleJwtAccessTokenParser = () => (claims) => {

const mapClaim = (claim) => (map = (v) => v, defaultValue) =>
Object.keys(claims).includes(claim) ? map(claims[claim]) : defaultValue;

return ({
clientId: mapClaim('sub')(),
scopes: mapClaim('scope')((claim) => claim.split(' '), []),
});
};

export const jwtAccessTokenVerifier = ({jwtVerifier, jwtAccessTokenParser}) => async (jwtAccessToken) => {
const claims = await jwtVerifier(jwtAccessToken);
return jwtAccessTokenParser(claims);
};
Loading