@openstax/ts-utils 1.25.5 → 1.26.2

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.
@@ -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>, appProvider?: {
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, appProvider) => {
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 = appProvider === null || appProvider === void 0 ? void 0 : appProvider.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
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 = appProvider === null || appProvider === void 0 ? void 0 : appProvider.logger) === null || _b === void 0 ? void 0 : _b.createSubContext();
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
- 'X-Request-ID': requestId,
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, appProvider) => {
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, appProvider)])));
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>(...[token, validator]: T extends undefined ? [string] : [string, (input: any) => T]) => Promise<T extends undefined ? jwt.JwtPayload : T>;
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
- const verify = (...[token, validator]) => new Promise((resolve, reject) => jwt.verify(token, getKey, {}, (err, payload) => {
38
- if (err && err instanceof TokenExpiredError) {
39
- reject(new SessionExpiredError());
40
- }
41
- else if (err) {
42
- reject(err);
43
- }
44
- else if (typeof payload !== 'object') {
45
- reject(new Error('received JWT token with unexpected non-JSON payload'));
46
- }
47
- else if (!payload.sub) {
48
- reject(new Error('JWT payload missing sub claim'));
49
- }
50
- else {
51
- // we are migrating away from json encoding all the parameters into the `sub` claim
52
- // and into using separate claims for each parameter. in transition, we check if the sub
53
- // is json and return it if it is. this is still a breaking change when using this
54
- // utility because applications no longer need to independently json parse the result
55
- // starting now.
56
- const parsed = payload;
57
- try {
58
- const jsonSubContents = JSON.parse(payload.sub);
59
- Object.assign(parsed, jsonSubContents);
60
- }
61
- catch (e) { } // eslint-disable-line no-empty
62
- // conditional return types are annoying
63
- resolve((validator ? validator(parsed) : parsed));
64
- }
65
- }));
66
- return { verify };
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,8 +1,9 @@
1
+ export declare type FieldType = string | string[] | number | boolean;
1
2
  export declare type Filter = {
2
3
  key: string;
3
- value: string | string[] | boolean;
4
+ value: FieldType;
4
5
  } | {
5
- terms: Record<string, string | string[] | number | boolean>;
6
+ terms: Record<string, FieldType>;
6
7
  } | {
7
8
  exists: {
8
9
  field: string;
@@ -7,6 +7,7 @@ export declare const memorySearchTheBadWay: () => <T>({ store }: {
7
7
  deleteIndexIfExists: () => Promise<undefined>;
8
8
  ensureIndexCreated: () => Promise<undefined>;
9
9
  index: (_options: IndexOptions<T>) => Promise<undefined>;
10
+ bulkIndex: (_items: IndexOptions<T>[]) => Promise<undefined>;
10
11
  search: (options: SearchOptions) => Promise<{
11
12
  items: T[];
12
13
  pageSize: number;
@@ -9,37 +9,56 @@ 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.key.toString().split('.').reduce((result, key) => result[key], document);
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.
16
42
  deleteIndexIfExists: async () => undefined,
17
43
  ensureIndexCreated: async () => undefined,
18
44
  index: async (_options) => undefined,
45
+ bulkIndex: async (_items) => undefined,
19
46
  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
47
  const results = (await store.loadAllDocumentsTheBadWay())
22
48
  .map(document => {
23
49
  let weight = 0;
24
50
  const matchFilters = (filters, matchType) => {
25
51
  for (const field of filters) {
26
- if (!('key' in field && 'value' in field)) {
27
- console.warn('local search only supports key/value filters');
28
- continue;
29
- }
30
- const docValues = coerceArray(resolveField(document, field));
31
- const coerceValue = getFieldType(field) === 'boolean'
32
- ? (input) => {
33
- if ([true, 'true', '1', 1].includes(input)) {
34
- return true;
35
- }
36
- if ([false, 'false', '0', 0, ''].includes(input)) {
37
- return false;
38
- }
39
- throw new InvalidRequestError('input is not a valid boolean filter');
40
- }
41
- : (x) => x;
42
- const hasMatch = coerceArray(field.value).map(coerceValue).some(v => docValues.includes(v));
52
+ const filter = ('key' in field && 'value' in field)
53
+ ? { terms: { [field.key]: field.value } }
54
+ : field;
55
+ let hasMatch;
56
+ if ('terms' in filter)
57
+ hasMatch = matchTerms(options, filter.terms, document);
58
+ else if ('exists' in filter)
59
+ hasMatch = matchExists(filter.exists, document);
60
+ else
61
+ throw new InvalidRequestError('invalid filter type');
43
62
  if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
44
63
  return false;
45
64
  }
@@ -54,7 +73,7 @@ export const memorySearchTheBadWay = () => ({ store }) => {
54
73
  if (field.type !== undefined && field.type !== 'text') {
55
74
  continue;
56
75
  }
57
- const value = resolveField(document, field);
76
+ const value = resolveField(document, field.key);
58
77
  if (value === undefined || value === null) {
59
78
  continue;
60
79
  }
@@ -85,8 +104,8 @@ export const memorySearchTheBadWay = () => ({ store }) => {
85
104
  .filter(r => !options.query || r.weight >= MIN_MATCH);
86
105
  results.sort((a, b) => {
87
106
  for (const sort of (options.sort || [])) {
88
- const aValue = resolveField(a.document, { key: sort.key });
89
- const bValue = resolveField(b.document, { key: sort.key });
107
+ const aValue = resolveField(a.document, sort.key);
108
+ const bValue = resolveField(b.document, sort.key);
90
109
  if (aValue < bValue) {
91
110
  return sort.order === 'asc' ? -1 : 1;
92
111
  }
@@ -19,6 +19,7 @@ export declare const openSearchService: <C extends string = "deployed">(initiali
19
19
  ensureIndexCreated: () => Promise<void>;
20
20
  deleteIndexIfExists: () => Promise<void>;
21
21
  index: (params: IndexOptions<T>) => Promise<void>;
22
+ bulkIndex: (items: IndexOptions<T>[]) => Promise<void>;
22
23
  search: (options: SearchOptions) => Promise<{
23
24
  items: Exclude<T, undefined>[];
24
25
  pageSize: number;
@@ -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({
@@ -50,6 +60,17 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
50
60
  refresh: true
51
61
  });
52
62
  };
63
+ const bulkIndex = async (items) => {
64
+ const openSearchClient = await client();
65
+ await openSearchClient.bulk({
66
+ index: indexConfig.name,
67
+ body: items.flatMap((item) => [
68
+ { index: { _id: item.id } },
69
+ item.body
70
+ ]),
71
+ refresh: true
72
+ });
73
+ };
53
74
  const search = async (options) => {
54
75
  const body = {
55
76
  query: { bool: {} },
@@ -64,45 +85,16 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
64
85
  }
65
86
  };
66
87
  }
67
- const { must_not } = options;
88
+ const { must_not, should } = options;
68
89
  const must = 'filter' in options ? options.filter : options.must;
69
90
  if (must && must.length > 0) {
70
- body.query.bool.filter = [];
71
- must.forEach((filter) => {
72
- if ('key' in filter && 'value' in filter) {
73
- const { key } = filter;
74
- const values = filter.value instanceof Array ? filter.value : [filter.value];
75
- body.query.bool.filter.push({ terms: { [key]: values } });
76
- }
77
- else {
78
- body.query.bool.filter.push(filter);
79
- }
80
- });
91
+ body.query.bool.filter = must.map(mapFilter);
81
92
  }
82
93
  if (must_not && must_not.length > 0) {
83
- body.query.bool.must_not = [];
84
- must_not.forEach((filter) => {
85
- if ('key' in filter && 'value' in filter) {
86
- const { key } = filter;
87
- const values = filter.value instanceof Array ? filter.value : [filter.value];
88
- values.forEach((value) => body.query.bool.must_not.push({ term: { [key]: value } }));
89
- }
90
- else {
91
- body.query.bool.must_not.push(filter);
92
- }
93
- });
94
+ body.query.bool.must_not = must_not.map(mapFilter);
94
95
  }
95
- if (options.should && options.should.length > 0) {
96
- body.query.bool.should = options.should.map(filter => {
97
- if ('key' in filter && 'value' in filter) {
98
- const { key } = filter;
99
- const values = filter.value instanceof Array ? filter.value : [filter.value];
100
- return { terms: { [key]: values } };
101
- }
102
- else {
103
- return filter;
104
- }
105
- });
96
+ if (should && should.length > 0) {
97
+ body.query.bool.should = should.map(mapFilter);
106
98
  body.query.bool.minimum_should_match = 1;
107
99
  }
108
100
  if (options.sort && options.sort.length > 0) {
@@ -128,6 +120,6 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
128
120
  const totalPages = Math.ceil(totalItems / pageSize) || 1;
129
121
  return { items, pageSize, currentPage, totalItems, totalPages };
130
122
  };
131
- return { ensureIndexCreated, deleteIndexIfExists, index, search };
123
+ return { ensureIndexCreated, deleteIndexIfExists, index, bulkIndex, search };
132
124
  };
133
125
  };