@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.
@@ -0,0 +1 @@
1
+ content
@@ -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: {
@@ -48,7 +48,7 @@ const loadResponse = (response) => () => {
48
48
  }
49
49
  };
50
50
  exports.loadResponse = loadResponse;
51
- const makeRouteClient = (initializer, config, route, appProvider) => {
51
+ const makeRouteClient = (initializer, config, route, app) => {
52
52
  /* TODO this duplicates code with makeRenderRouteUrl, reuse that */
53
53
  const renderUrl = async ({ params, query }) => {
54
54
  const apiBase = await (0, config_1.resolveConfigValue)(config.apiBase);
@@ -61,9 +61,9 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
61
61
  const { fetch } = initializer;
62
62
  const url = await renderUrl({ params, query });
63
63
  const body = payload ? JSON.stringify(payload) : undefined;
64
- const baseOptions = (0, __1.merge)((await ((_a = appProvider === null || appProvider === void 0 ? void 0 : appProvider.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
64
+ const baseOptions = (0, __1.merge)((await ((_a = app === null || app === void 0 ? void 0 : app.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
65
65
  const requestId = (0, uuid_1.v4)();
66
- const requestLogger = (_b = appProvider === null || appProvider === void 0 ? void 0 : appProvider.logger) === null || _b === void 0 ? void 0 : _b.createSubContext();
66
+ const requestLogger = (_b = app === null || app === void 0 ? void 0 : app.logger) === null || _b === void 0 ? void 0 : _b.createSubContext();
67
67
  requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.setContext({ requestId, url, timeStamp: new Date().getTime() });
68
68
  const fetcher = (0, fetchStatusRetry_1.fetchStatusRetry)(fetch, { retries: 1, status: [502], logger: requestLogger });
69
69
  requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.log('Request Initiated', logger_1.Level.Info);
@@ -73,7 +73,10 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
73
73
  headers: {
74
74
  ...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
75
75
  ...(body ? { 'content-type': 'application/json' } : {}),
76
- 'X-Request-ID': requestId,
76
+ 'x-request-id': requestId,
77
+ ...((app === null || app === void 0 ? void 0 : app.launchToken) ? {
78
+ 'x-launch-token': app.launchToken,
79
+ } : {}),
77
80
  }
78
81
  })).then(response => {
79
82
  if (response.status === 401) {
@@ -98,8 +101,8 @@ const makeRouteClient = (initializer, config, route, appProvider) => {
98
101
  routeClient.renderUrl = renderUrl;
99
102
  return routeClient;
100
103
  };
101
- const createApiGateway = (initializer) => (config, routes, appProvider) => {
104
+ const createApiGateway = (initializer) => (config, routes, app) => {
102
105
  return Object.fromEntries(Object.entries(routes)
103
- .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, appProvider)])));
106
+ .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, app)])));
104
107
  };
105
108
  exports.createApiGateway = createApiGateway;
@@ -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 {};
@@ -60,35 +60,50 @@ const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvider) => {
60
60
  callback(error);
61
61
  }
62
62
  };
63
- const verify = (...[token, validator]) => new Promise((resolve, reject) => jsonwebtoken_1.default.verify(token, getKey, {}, (err, payload) => {
64
- if (err && err instanceof jsonwebtoken_1.TokenExpiredError) {
65
- reject(new errors_1.SessionExpiredError());
66
- }
67
- else if (err) {
68
- reject(err);
69
- }
70
- else if (typeof payload !== 'object') {
71
- reject(new Error('received JWT token with unexpected non-JSON payload'));
72
- }
73
- else if (!payload.sub) {
74
- reject(new Error('JWT payload missing sub claim'));
75
- }
76
- else {
77
- // we are migrating away from json encoding all the parameters into the `sub` claim
78
- // and into using separate claims for each parameter. in transition, we check if the sub
79
- // is json and return it if it is. this is still a breaking change when using this
80
- // utility because applications no longer need to independently json parse the result
81
- // starting now.
82
- const parsed = payload;
83
- try {
84
- const jsonSubContents = JSON.parse(payload.sub);
85
- Object.assign(parsed, jsonSubContents);
86
- }
87
- catch (e) { } // eslint-disable-line no-empty
88
- // conditional return types are annoying
89
- resolve((validator ? validator(parsed) : parsed));
90
- }
91
- }));
92
- return { verify };
63
+ return (_services, getDefaultToken) => {
64
+ const verify = (...args) => {
65
+ const [inputToken, validator] = args.length === 1
66
+ ? typeof args[0] === 'string'
67
+ ? [args[0], undefined]
68
+ : [undefined, args[0]]
69
+ : args;
70
+ return new Promise((resolve, reject) => {
71
+ const token = inputToken !== null && inputToken !== void 0 ? inputToken : getDefaultToken === null || getDefaultToken === void 0 ? void 0 : getDefaultToken();
72
+ if (!token) {
73
+ return reject(new errors_1.InvalidRequestError('Missing token for launch verification'));
74
+ }
75
+ return jsonwebtoken_1.default.verify(token, getKey, {}, (err, payload) => {
76
+ if (err && err instanceof jsonwebtoken_1.TokenExpiredError) {
77
+ reject(new errors_1.SessionExpiredError());
78
+ }
79
+ else if (err) {
80
+ reject(err);
81
+ }
82
+ else if (typeof payload !== 'object') {
83
+ reject(new Error('received JWT token with unexpected non-JSON payload'));
84
+ }
85
+ else if (!payload.sub) {
86
+ reject(new Error('JWT payload missing sub claim'));
87
+ }
88
+ else {
89
+ // we are migrating away from json encoding all the parameters into the `sub` claim
90
+ // and into using separate claims for each parameter. in transition, we check if the sub
91
+ // is json and return it if it is. this is still a breaking change when using this
92
+ // utility because applications no longer need to independently json parse the result
93
+ // starting now.
94
+ const parsed = payload;
95
+ try {
96
+ const jsonSubContents = JSON.parse(payload.sub);
97
+ Object.assign(parsed, jsonSubContents);
98
+ }
99
+ catch (e) { } // eslint-disable-line no-empty
100
+ // conditional return types are annoying
101
+ resolve((validator ? validator(parsed) : parsed));
102
+ }
103
+ });
104
+ });
105
+ };
106
+ return { verify };
107
+ };
93
108
  };
94
109
  exports.createLaunchVerifier = createLaunchVerifier;
@@ -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;
@@ -12,37 +12,56 @@ var MatchType;
12
12
  MatchType[MatchType["MustNot"] = 1] = "MustNot";
13
13
  MatchType[MatchType["Should"] = 2] = "Should";
14
14
  })(MatchType || (MatchType = {}));
15
- const resolveField = (document, field) => field.key.toString().split('.').reduce((result, key) => result[key], document);
15
+ const resolveField = (document, field) => field.split('.').reduce((result, key) => result[key], document);
16
+ const matchExists = (exists, document) => {
17
+ const value = resolveField(document, exists.field);
18
+ return value !== undefined && value !== null;
19
+ };
20
+ const matchTerms = (options, terms, document) => {
21
+ const getFieldType = (field) => { var _a; return (_a = options.fields.find(f => f.key == field)) === null || _a === void 0 ? void 0 : _a.type; };
22
+ for (const key in terms) {
23
+ const docValues = (0, __1.coerceArray)(resolveField(document, key));
24
+ const coerceValue = getFieldType(key) === 'boolean'
25
+ ? (input) => {
26
+ if ([true, 'true', '1', 1].includes(input)) {
27
+ return true;
28
+ }
29
+ if ([false, 'false', '0', 0, ''].includes(input)) {
30
+ return false;
31
+ }
32
+ throw new errors_1.InvalidRequestError('input is not a valid boolean filter');
33
+ }
34
+ : (x) => x;
35
+ const hasMatch = (0, __1.coerceArray)(terms[key]).map(coerceValue).some(v => docValues.includes(v));
36
+ if (!hasMatch) {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ };
16
42
  const memorySearchTheBadWay = () => ({ store }) => {
17
43
  return {
18
44
  // This method is intentionally stubbed because index deletion is not applicable for in-memory storage.
19
45
  deleteIndexIfExists: async () => undefined,
20
46
  ensureIndexCreated: async () => undefined,
21
47
  index: async (_options) => undefined,
48
+ bulkIndex: async (_items) => undefined,
22
49
  search: async (options) => {
23
- const getFieldType = (field) => { var _a; return (_a = options.fields.find(f => f.key == field.key)) === null || _a === void 0 ? void 0 : _a.type; };
24
50
  const results = (await store.loadAllDocumentsTheBadWay())
25
51
  .map(document => {
26
52
  let weight = 0;
27
53
  const matchFilters = (filters, matchType) => {
28
54
  for (const field of filters) {
29
- if (!('key' in field && 'value' in field)) {
30
- console.warn('local search only supports key/value filters');
31
- continue;
32
- }
33
- const docValues = (0, __1.coerceArray)(resolveField(document, field));
34
- const coerceValue = getFieldType(field) === 'boolean'
35
- ? (input) => {
36
- if ([true, 'true', '1', 1].includes(input)) {
37
- return true;
38
- }
39
- if ([false, 'false', '0', 0, ''].includes(input)) {
40
- return false;
41
- }
42
- throw new errors_1.InvalidRequestError('input is not a valid boolean filter');
43
- }
44
- : (x) => x;
45
- const hasMatch = (0, __1.coerceArray)(field.value).map(coerceValue).some(v => docValues.includes(v));
55
+ const filter = ('key' in field && 'value' in field)
56
+ ? { terms: { [field.key]: field.value } }
57
+ : field;
58
+ let hasMatch;
59
+ if ('terms' in filter)
60
+ hasMatch = matchTerms(options, filter.terms, document);
61
+ else if ('exists' in filter)
62
+ hasMatch = matchExists(filter.exists, document);
63
+ else
64
+ throw new errors_1.InvalidRequestError('invalid filter type');
46
65
  if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
47
66
  return false;
48
67
  }
@@ -57,7 +76,7 @@ const memorySearchTheBadWay = () => ({ store }) => {
57
76
  if (field.type !== undefined && field.type !== 'text') {
58
77
  continue;
59
78
  }
60
- const value = resolveField(document, field);
79
+ const value = resolveField(document, field.key);
61
80
  if (value === undefined || value === null) {
62
81
  continue;
63
82
  }
@@ -88,8 +107,8 @@ const memorySearchTheBadWay = () => ({ store }) => {
88
107
  .filter(r => !options.query || r.weight >= MIN_MATCH);
89
108
  results.sort((a, b) => {
90
109
  for (const sort of (options.sort || [])) {
91
- const aValue = resolveField(a.document, { key: sort.key });
92
- const bValue = resolveField(b.document, { key: sort.key });
110
+ const aValue = resolveField(a.document, sort.key);
111
+ const bValue = resolveField(b.document, sort.key);
93
112
  if (aValue < bValue) {
94
113
  return sort.order === 'asc' ? -1 : 1;
95
114
  }
@@ -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;
@@ -8,6 +8,16 @@ const aws_1 = require("@opensearch-project/opensearch/aws");
8
8
  const config_1 = require("../../config");
9
9
  const guards_1 = require("../../guards");
10
10
  const helpers_1 = require("../../misc/helpers");
11
+ const mapFilter = (filter) => {
12
+ if ('key' in filter && 'value' in filter) {
13
+ const { key } = filter;
14
+ const values = filter.value instanceof Array ? filter.value : [filter.value];
15
+ return { terms: { [key]: values } };
16
+ }
17
+ else {
18
+ return filter;
19
+ }
20
+ };
11
21
  const openSearchService = (initializer = {}) => (configProvider) => {
12
22
  const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'deployed')];
13
23
  const client = (0, helpers_1.once)(async () => new opensearch_1.Client({
@@ -53,6 +63,17 @@ const openSearchService = (initializer = {}) => (configProvider) => {
53
63
  refresh: true
54
64
  });
55
65
  };
66
+ const bulkIndex = async (items) => {
67
+ const openSearchClient = await client();
68
+ await openSearchClient.bulk({
69
+ index: indexConfig.name,
70
+ body: items.flatMap((item) => [
71
+ { index: { _id: item.id } },
72
+ item.body
73
+ ]),
74
+ refresh: true
75
+ });
76
+ };
56
77
  const search = async (options) => {
57
78
  const body = {
58
79
  query: { bool: {} },
@@ -67,45 +88,16 @@ const openSearchService = (initializer = {}) => (configProvider) => {
67
88
  }
68
89
  };
69
90
  }
70
- const { must_not } = options;
91
+ const { must_not, should } = options;
71
92
  const must = 'filter' in options ? options.filter : options.must;
72
93
  if (must && must.length > 0) {
73
- body.query.bool.filter = [];
74
- must.forEach((filter) => {
75
- if ('key' in filter && 'value' in filter) {
76
- const { key } = filter;
77
- const values = filter.value instanceof Array ? filter.value : [filter.value];
78
- body.query.bool.filter.push({ terms: { [key]: values } });
79
- }
80
- else {
81
- body.query.bool.filter.push(filter);
82
- }
83
- });
94
+ body.query.bool.filter = must.map(mapFilter);
84
95
  }
85
96
  if (must_not && must_not.length > 0) {
86
- body.query.bool.must_not = [];
87
- must_not.forEach((filter) => {
88
- if ('key' in filter && 'value' in filter) {
89
- const { key } = filter;
90
- const values = filter.value instanceof Array ? filter.value : [filter.value];
91
- values.forEach((value) => body.query.bool.must_not.push({ term: { [key]: value } }));
92
- }
93
- else {
94
- body.query.bool.must_not.push(filter);
95
- }
96
- });
97
+ body.query.bool.must_not = must_not.map(mapFilter);
97
98
  }
98
- if (options.should && options.should.length > 0) {
99
- body.query.bool.should = options.should.map(filter => {
100
- if ('key' in filter && 'value' in filter) {
101
- const { key } = filter;
102
- const values = filter.value instanceof Array ? filter.value : [filter.value];
103
- return { terms: { [key]: values } };
104
- }
105
- else {
106
- return filter;
107
- }
108
- });
99
+ if (should && should.length > 0) {
100
+ body.query.bool.should = should.map(mapFilter);
109
101
  body.query.bool.minimum_should_match = 1;
110
102
  }
111
103
  if (options.sort && options.sort.length > 0) {
@@ -131,7 +123,7 @@ const openSearchService = (initializer = {}) => (configProvider) => {
131
123
  const totalPages = Math.ceil(totalItems / pageSize) || 1;
132
124
  return { items, pageSize, currentPage, totalItems, totalPages };
133
125
  };
134
- return { ensureIndexCreated, deleteIndexIfExists, index, search };
126
+ return { ensureIndexCreated, deleteIndexIfExists, index, bulkIndex, search };
135
127
  };
136
128
  };
137
129
  exports.openSearchService = openSearchService;