@openstax/ts-utils 1.25.4 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/services/apiGateway/index.d.ts +2 -1
- package/dist/cjs/services/apiGateway/index.js +9 -6
- package/dist/cjs/services/launchParams/verifier.d.ts +3 -3
- package/dist/cjs/services/launchParams/verifier.js +45 -30
- package/dist/cjs/services/searchProvider/index.d.ts +8 -1
- package/dist/cjs/services/searchProvider/memorySearchTheBadWay.js +40 -18
- package/dist/cjs/services/searchProvider/openSearch.js +16 -20
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/services/apiGateway/index.d.ts +2 -1
- package/dist/esm/services/apiGateway/index.js +9 -6
- package/dist/esm/services/launchParams/verifier.d.ts +3 -3
- package/dist/esm/services/launchParams/verifier.js +46 -31
- package/dist/esm/services/searchProvider/index.d.ts +8 -1
- package/dist/esm/services/searchProvider/memorySearchTheBadWay.js +40 -18
- package/dist/esm/services/searchProvider/openSearch.js +16 -20
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -50,11 +50,12 @@ export declare const loadResponse: (response: Response) => () => Promise<any>;
|
|
|
50
50
|
interface MakeApiGateway<F> {
|
|
51
51
|
<Ru>(config: ConfigProviderForConfig<{
|
|
52
52
|
apiBase: string;
|
|
53
|
-
}>, routes: MapRoutesToConfig<Ru>,
|
|
53
|
+
}>, routes: MapRoutesToConfig<Ru>, app?: {
|
|
54
54
|
authProvider?: {
|
|
55
55
|
getAuthorizedFetchConfig: () => Promise<ConfigForFetch<F>>;
|
|
56
56
|
};
|
|
57
57
|
logger?: Logger;
|
|
58
|
+
launchToken?: string;
|
|
58
59
|
}): MapRoutesToClient<Ru>;
|
|
59
60
|
}
|
|
60
61
|
export declare const createApiGateway: <F extends GenericFetch<import("../../fetch").FetchConfig, Response>>(initializer: {
|
|
@@ -18,7 +18,7 @@ export const loadResponse = (response) => () => {
|
|
|
18
18
|
throw new Error(`unknown content type ${contentType}`);
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
-
const makeRouteClient = (initializer, config, route,
|
|
21
|
+
const makeRouteClient = (initializer, config, route, app) => {
|
|
22
22
|
/* TODO this duplicates code with makeRenderRouteUrl, reuse that */
|
|
23
23
|
const renderUrl = async ({ params, query }) => {
|
|
24
24
|
const apiBase = await resolveConfigValue(config.apiBase);
|
|
@@ -31,9 +31,9 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
|
|
|
31
31
|
const { fetch } = initializer;
|
|
32
32
|
const url = await renderUrl({ params, query });
|
|
33
33
|
const body = payload ? JSON.stringify(payload) : undefined;
|
|
34
|
-
const baseOptions = merge((await ((_a =
|
|
34
|
+
const baseOptions = merge((await ((_a = app === null || app === void 0 ? void 0 : app.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
|
|
35
35
|
const requestId = uuid();
|
|
36
|
-
const requestLogger = (_b =
|
|
36
|
+
const requestLogger = (_b = app === null || app === void 0 ? void 0 : app.logger) === null || _b === void 0 ? void 0 : _b.createSubContext();
|
|
37
37
|
requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.setContext({ requestId, url, timeStamp: new Date().getTime() });
|
|
38
38
|
const fetcher = fetchStatusRetry(fetch, { retries: 1, status: [502], logger: requestLogger });
|
|
39
39
|
requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.log('Request Initiated', Level.Info);
|
|
@@ -43,7 +43,10 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
|
|
|
43
43
|
headers: {
|
|
44
44
|
...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
|
|
45
45
|
...(body ? { 'content-type': 'application/json' } : {}),
|
|
46
|
-
'
|
|
46
|
+
'x-request-id': requestId,
|
|
47
|
+
...((app === null || app === void 0 ? void 0 : app.launchToken) ? {
|
|
48
|
+
'x-launch-token': app.launchToken,
|
|
49
|
+
} : {}),
|
|
47
50
|
}
|
|
48
51
|
})).then(response => {
|
|
49
52
|
if (response.status === 401) {
|
|
@@ -68,7 +71,7 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
|
|
|
68
71
|
routeClient.renderUrl = renderUrl;
|
|
69
72
|
return routeClient;
|
|
70
73
|
};
|
|
71
|
-
export const createApiGateway = (initializer) => (config, routes,
|
|
74
|
+
export const createApiGateway = (initializer) => (config, routes, app) => {
|
|
72
75
|
return Object.fromEntries(Object.entries(routes)
|
|
73
|
-
.map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig,
|
|
76
|
+
.map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, app)])));
|
|
74
77
|
};
|
|
@@ -15,8 +15,8 @@ interface Initializer<C> {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const createLaunchVerifier: <C extends string = "launch">({ configSpace, fetcher }: Initializer<C>) => (configProvider: { [key in C]: {
|
|
17
17
|
trustedDomain: import("../../config").ConfigValueProvider<string>;
|
|
18
|
-
}; }) => {
|
|
19
|
-
verify: <T = undefined>(...
|
|
18
|
+
}; }) => (_services: {}, getDefaultToken?: (() => string) | undefined) => {
|
|
19
|
+
verify: <T = undefined>(...args: T extends undefined ? [] | [string] : [(input: any) => T] | [string, (input: any) => T]) => Promise<T extends undefined ? jwt.JwtPayload : T>;
|
|
20
20
|
};
|
|
21
|
-
export declare type LaunchVerifier = ReturnType<ReturnType<typeof createLaunchVerifier
|
|
21
|
+
export declare type LaunchVerifier = ReturnType<ReturnType<ReturnType<typeof createLaunchVerifier>>>;
|
|
22
22
|
export {};
|
|
@@ -2,7 +2,7 @@ import jwt, { TokenExpiredError } from 'jsonwebtoken';
|
|
|
2
2
|
import { JwksClient } from 'jwks-rsa';
|
|
3
3
|
import { memoize } from '../..';
|
|
4
4
|
import { resolveConfigValue } from '../../config';
|
|
5
|
-
import { SessionExpiredError } from '../../errors';
|
|
5
|
+
import { InvalidRequestError, SessionExpiredError } from '../../errors';
|
|
6
6
|
import { ifDefined } from '../../guards';
|
|
7
7
|
/**
|
|
8
8
|
* Creates a class that can verify launch params
|
|
@@ -34,34 +34,49 @@ export const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvide
|
|
|
34
34
|
callback(error);
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
37
|
+
return (_services, getDefaultToken) => {
|
|
38
|
+
const verify = (...args) => {
|
|
39
|
+
const [inputToken, validator] = args.length === 1
|
|
40
|
+
? typeof args[0] === 'string'
|
|
41
|
+
? [args[0], undefined]
|
|
42
|
+
: [undefined, args[0]]
|
|
43
|
+
: args;
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const token = inputToken !== null && inputToken !== void 0 ? inputToken : getDefaultToken === null || getDefaultToken === void 0 ? void 0 : getDefaultToken();
|
|
46
|
+
if (!token) {
|
|
47
|
+
return reject(new InvalidRequestError('Missing token for launch verification'));
|
|
48
|
+
}
|
|
49
|
+
return jwt.verify(token, getKey, {}, (err, payload) => {
|
|
50
|
+
if (err && err instanceof TokenExpiredError) {
|
|
51
|
+
reject(new SessionExpiredError());
|
|
52
|
+
}
|
|
53
|
+
else if (err) {
|
|
54
|
+
reject(err);
|
|
55
|
+
}
|
|
56
|
+
else if (typeof payload !== 'object') {
|
|
57
|
+
reject(new Error('received JWT token with unexpected non-JSON payload'));
|
|
58
|
+
}
|
|
59
|
+
else if (!payload.sub) {
|
|
60
|
+
reject(new Error('JWT payload missing sub claim'));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// we are migrating away from json encoding all the parameters into the `sub` claim
|
|
64
|
+
// and into using separate claims for each parameter. in transition, we check if the sub
|
|
65
|
+
// is json and return it if it is. this is still a breaking change when using this
|
|
66
|
+
// utility because applications no longer need to independently json parse the result
|
|
67
|
+
// starting now.
|
|
68
|
+
const parsed = payload;
|
|
69
|
+
try {
|
|
70
|
+
const jsonSubContents = JSON.parse(payload.sub);
|
|
71
|
+
Object.assign(parsed, jsonSubContents);
|
|
72
|
+
}
|
|
73
|
+
catch (e) { } // eslint-disable-line no-empty
|
|
74
|
+
// conditional return types are annoying
|
|
75
|
+
resolve((validator ? validator(parsed) : parsed));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
return { verify };
|
|
81
|
+
};
|
|
67
82
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
export declare type FieldType = string | string[] | number | boolean;
|
|
1
2
|
export declare type Filter = {
|
|
2
3
|
key: string;
|
|
3
|
-
value:
|
|
4
|
+
value: FieldType;
|
|
5
|
+
} | {
|
|
6
|
+
terms: Record<string, FieldType>;
|
|
7
|
+
} | {
|
|
8
|
+
exists: {
|
|
9
|
+
field: string;
|
|
10
|
+
};
|
|
4
11
|
};
|
|
5
12
|
declare type Field = {
|
|
6
13
|
key: string;
|
|
@@ -9,7 +9,33 @@ var MatchType;
|
|
|
9
9
|
MatchType[MatchType["MustNot"] = 1] = "MustNot";
|
|
10
10
|
MatchType[MatchType["Should"] = 2] = "Should";
|
|
11
11
|
})(MatchType || (MatchType = {}));
|
|
12
|
-
const resolveField = (document, field) => field.
|
|
12
|
+
const resolveField = (document, field) => field.split('.').reduce((result, key) => result[key], document);
|
|
13
|
+
const matchExists = (exists, document) => {
|
|
14
|
+
const value = resolveField(document, exists.field);
|
|
15
|
+
return value !== undefined && value !== null;
|
|
16
|
+
};
|
|
17
|
+
const matchTerms = (options, terms, document) => {
|
|
18
|
+
const getFieldType = (field) => { var _a; return (_a = options.fields.find(f => f.key == field)) === null || _a === void 0 ? void 0 : _a.type; };
|
|
19
|
+
for (const key in terms) {
|
|
20
|
+
const docValues = coerceArray(resolveField(document, key));
|
|
21
|
+
const coerceValue = getFieldType(key) === 'boolean'
|
|
22
|
+
? (input) => {
|
|
23
|
+
if ([true, 'true', '1', 1].includes(input)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if ([false, 'false', '0', 0, ''].includes(input)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
throw new InvalidRequestError('input is not a valid boolean filter');
|
|
30
|
+
}
|
|
31
|
+
: (x) => x;
|
|
32
|
+
const hasMatch = coerceArray(terms[key]).map(coerceValue).some(v => docValues.includes(v));
|
|
33
|
+
if (!hasMatch) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
13
39
|
export const memorySearchTheBadWay = () => ({ store }) => {
|
|
14
40
|
return {
|
|
15
41
|
// This method is intentionally stubbed because index deletion is not applicable for in-memory storage.
|
|
@@ -17,25 +43,21 @@ export const memorySearchTheBadWay = () => ({ store }) => {
|
|
|
17
43
|
ensureIndexCreated: async () => undefined,
|
|
18
44
|
index: async (_options) => undefined,
|
|
19
45
|
search: async (options) => {
|
|
20
|
-
const getFieldType = (field) => { var _a; return (_a = options.fields.find(f => f.key == field.key)) === null || _a === void 0 ? void 0 : _a.type; };
|
|
21
46
|
const results = (await store.loadAllDocumentsTheBadWay())
|
|
22
47
|
.map(document => {
|
|
23
48
|
let weight = 0;
|
|
24
49
|
const matchFilters = (filters, matchType) => {
|
|
25
50
|
for (const field of filters) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
: (x) => x;
|
|
38
|
-
const hasMatch = coerceArray(field.value).map(coerceValue).some(v => docValues.includes(v));
|
|
51
|
+
const filter = ('key' in field && 'value' in field)
|
|
52
|
+
? { terms: { [field.key]: field.value } }
|
|
53
|
+
: field;
|
|
54
|
+
let hasMatch;
|
|
55
|
+
if ('terms' in filter)
|
|
56
|
+
hasMatch = matchTerms(options, filter.terms, document);
|
|
57
|
+
else if ('exists' in filter)
|
|
58
|
+
hasMatch = matchExists(filter.exists, document);
|
|
59
|
+
else
|
|
60
|
+
throw new InvalidRequestError('invalid filter type');
|
|
39
61
|
if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
|
|
40
62
|
return false;
|
|
41
63
|
}
|
|
@@ -50,7 +72,7 @@ export const memorySearchTheBadWay = () => ({ store }) => {
|
|
|
50
72
|
if (field.type !== undefined && field.type !== 'text') {
|
|
51
73
|
continue;
|
|
52
74
|
}
|
|
53
|
-
const value = resolveField(document, field);
|
|
75
|
+
const value = resolveField(document, field.key);
|
|
54
76
|
if (value === undefined || value === null) {
|
|
55
77
|
continue;
|
|
56
78
|
}
|
|
@@ -81,8 +103,8 @@ export const memorySearchTheBadWay = () => ({ store }) => {
|
|
|
81
103
|
.filter(r => !options.query || r.weight >= MIN_MATCH);
|
|
82
104
|
results.sort((a, b) => {
|
|
83
105
|
for (const sort of (options.sort || [])) {
|
|
84
|
-
const aValue = resolveField(a.document,
|
|
85
|
-
const bValue = resolveField(b.document,
|
|
106
|
+
const aValue = resolveField(a.document, sort.key);
|
|
107
|
+
const bValue = resolveField(b.document, sort.key);
|
|
86
108
|
if (aValue < bValue) {
|
|
87
109
|
return sort.order === 'asc' ? -1 : 1;
|
|
88
110
|
}
|
|
@@ -5,6 +5,16 @@ import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws';
|
|
|
5
5
|
import { resolveConfigValue } from '../../config';
|
|
6
6
|
import { ifDefined, isDefined } from '../../guards';
|
|
7
7
|
import { once } from '../../misc/helpers';
|
|
8
|
+
const mapFilter = (filter) => {
|
|
9
|
+
if ('key' in filter && 'value' in filter) {
|
|
10
|
+
const { key } = filter;
|
|
11
|
+
const values = filter.value instanceof Array ? filter.value : [filter.value];
|
|
12
|
+
return { terms: { [key]: values } };
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return filter;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
8
18
|
export const openSearchService = (initializer = {}) => (configProvider) => {
|
|
9
19
|
const config = configProvider[ifDefined(initializer.configSpace, 'deployed')];
|
|
10
20
|
const client = once(async () => new Client({
|
|
@@ -54,6 +64,7 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
54
64
|
const body = {
|
|
55
65
|
query: { bool: {} },
|
|
56
66
|
track_total_hits: true,
|
|
67
|
+
size: pageSize
|
|
57
68
|
};
|
|
58
69
|
if (options.query) {
|
|
59
70
|
body.query.bool.must = {
|
|
@@ -63,30 +74,16 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
63
74
|
}
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
|
-
const { must_not } = options;
|
|
77
|
+
const { must_not, should } = options;
|
|
67
78
|
const must = 'filter' in options ? options.filter : options.must;
|
|
68
79
|
if (must && must.length > 0) {
|
|
69
|
-
body.query.bool.filter =
|
|
70
|
-
must.forEach((filter) => {
|
|
71
|
-
const { key } = filter;
|
|
72
|
-
const values = filter.value instanceof Array ? filter.value : [filter.value];
|
|
73
|
-
body.query.bool.filter.push({ terms: { [key]: values } });
|
|
74
|
-
});
|
|
80
|
+
body.query.bool.filter = must.map(mapFilter);
|
|
75
81
|
}
|
|
76
82
|
if (must_not && must_not.length > 0) {
|
|
77
|
-
body.query.bool.must_not =
|
|
78
|
-
must_not.forEach((filter) => {
|
|
79
|
-
const { key } = filter;
|
|
80
|
-
const values = filter.value instanceof Array ? filter.value : [filter.value];
|
|
81
|
-
values.forEach((value) => body.query.bool.must_not.push({ term: { [key]: value } }));
|
|
82
|
-
});
|
|
83
|
+
body.query.bool.must_not = must_not.map(mapFilter);
|
|
83
84
|
}
|
|
84
|
-
if (
|
|
85
|
-
body.query.bool.should =
|
|
86
|
-
const { key } = term;
|
|
87
|
-
const values = term.value instanceof Array ? term.value : [term.value];
|
|
88
|
-
return { terms: { [key]: values } };
|
|
89
|
-
});
|
|
85
|
+
if (should && should.length > 0) {
|
|
86
|
+
body.query.bool.should = should.map(mapFilter);
|
|
90
87
|
body.query.bool.minimum_should_match = 1;
|
|
91
88
|
}
|
|
92
89
|
if (options.sort && options.sort.length > 0) {
|
|
@@ -95,7 +92,6 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
95
92
|
}));
|
|
96
93
|
}
|
|
97
94
|
if (options.page) {
|
|
98
|
-
body.size = pageSize;
|
|
99
95
|
body.from = (options.page - 1) * pageSize;
|
|
100
96
|
}
|
|
101
97
|
const response = await (await client()).search({
|