@openstax/ts-utils 1.27.6 → 1.29.0

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.
Files changed (29) hide show
  1. package/dist/cjs/errors/index.d.ts +11 -0
  2. package/dist/cjs/errors/index.js +15 -1
  3. package/dist/cjs/fetch/fetchStatusRetry.d.ts +1 -0
  4. package/dist/cjs/fetch/fetchStatusRetry.js +18 -7
  5. package/dist/cjs/guards/index.d.ts +8 -0
  6. package/dist/cjs/guards/index.js +10 -1
  7. package/dist/cjs/services/documentStore/unversioned/dynamodb.d.ts +9 -0
  8. package/dist/cjs/services/documentStore/unversioned/dynamodb.js +67 -0
  9. package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +12 -1
  10. package/dist/cjs/services/documentStore/unversioned/file-system.js +49 -1
  11. package/dist/cjs/services/launchParams/verifier.d.ts +2 -0
  12. package/dist/cjs/services/launchParams/verifier.js +12 -4
  13. package/dist/cjs/services/searchProvider/openSearch.js +3 -0
  14. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/errors/index.d.ts +11 -0
  16. package/dist/esm/errors/index.js +13 -0
  17. package/dist/esm/fetch/fetchStatusRetry.d.ts +1 -0
  18. package/dist/esm/fetch/fetchStatusRetry.js +18 -7
  19. package/dist/esm/guards/index.d.ts +8 -0
  20. package/dist/esm/guards/index.js +8 -0
  21. package/dist/esm/services/documentStore/unversioned/dynamodb.d.ts +9 -0
  22. package/dist/esm/services/documentStore/unversioned/dynamodb.js +65 -1
  23. package/dist/esm/services/documentStore/unversioned/file-system.d.ts +12 -1
  24. package/dist/esm/services/documentStore/unversioned/file-system.js +50 -2
  25. package/dist/esm/services/launchParams/verifier.d.ts +2 -0
  26. package/dist/esm/services/launchParams/verifier.js +12 -4
  27. package/dist/esm/services/searchProvider/openSearch.js +3 -0
  28. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
  29. package/package.json +3 -1
@@ -75,3 +75,14 @@ export declare class SessionExpiredError extends Error {
75
75
  static matches: (e: any) => e is typeof SessionExpiredError;
76
76
  constructor(message?: string);
77
77
  }
78
+ /**
79
+ * Conflict error
80
+ *
81
+ * `ConflictError.matches(error)` is a reliable way to check if an error is a
82
+ * `ConflictError`; `instanceof` checks may not work if code is split into multiple bundles
83
+ */
84
+ export declare class ConflictError extends Error {
85
+ static readonly TYPE = "ConflictError";
86
+ static matches: (e: any) => e is typeof ConflictError;
87
+ constructor(message?: string);
88
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SessionExpiredError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.ValidationError = exports.InvalidRequestError = exports.isAppError = void 0;
3
+ exports.ConflictError = exports.SessionExpiredError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.ValidationError = exports.InvalidRequestError = exports.isAppError = void 0;
4
4
  /*
5
5
  * if code is split into multiple bundles, sometimes each bundle
6
6
  * will get its own definition of this module and then instanceof checks
@@ -107,3 +107,17 @@ class SessionExpiredError extends Error {
107
107
  exports.SessionExpiredError = SessionExpiredError;
108
108
  SessionExpiredError.TYPE = 'SessionExpiredError';
109
109
  SessionExpiredError.matches = errorIsType(SessionExpiredError);
110
+ /**
111
+ * Conflict error
112
+ *
113
+ * `ConflictError.matches(error)` is a reliable way to check if an error is a
114
+ * `ConflictError`; `instanceof` checks may not work if code is split into multiple bundles
115
+ */
116
+ class ConflictError extends Error {
117
+ constructor(message) {
118
+ super(message || ConflictError.TYPE);
119
+ }
120
+ }
121
+ exports.ConflictError = ConflictError;
122
+ ConflictError.TYPE = 'ConflictError';
123
+ ConflictError.matches = errorIsType(ConflictError);
@@ -2,6 +2,7 @@ import { RetryOptions } from '../misc/helpers';
2
2
  import { GenericFetch } from '.';
3
3
  interface Options extends RetryOptions {
4
4
  status?: number[];
5
+ timeout?: number;
5
6
  }
6
7
  export declare const fetchStatusRetry: (base: GenericFetch, options: Options) => GenericFetch;
7
8
  export {};
@@ -3,14 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchStatusRetry = void 0;
4
4
  const helpers_1 = require("../misc/helpers");
5
5
  const fetchStatusRetry = (base, options) => {
6
- return (...params) => (0, helpers_1.retryWithDelay)(() => base(...params).then(r => {
7
- var _a;
8
- if ((_a = options.status) === null || _a === void 0 ? void 0 : _a.includes(r.status)) {
9
- return r.text().then(text => {
10
- throw new Error(`fetch failed ${params[0]} -- ${r.status}: ${text}`);
6
+ return (...params) => (0, helpers_1.retryWithDelay)(() => {
7
+ const fetchPromise = base(...params).then(r => {
8
+ var _a;
9
+ if ((_a = options.status) === null || _a === void 0 ? void 0 : _a.includes(r.status)) {
10
+ return r.text().then(text => {
11
+ throw new Error(`fetch failed ${params[0]} -- ${r.status}: ${text}`);
12
+ });
13
+ }
14
+ return r;
15
+ });
16
+ if (options.timeout) {
17
+ const timeoutPromise = new Promise((_, reject) => {
18
+ setTimeout(() => {
19
+ reject(new Error(`fetch timeout after ${options.timeout}ms: ${params[0]}`));
20
+ }, options.timeout);
11
21
  });
22
+ return Promise.race([fetchPromise, timeoutPromise]);
12
23
  }
13
- return r;
14
- }), options);
24
+ return fetchPromise;
25
+ }, options);
15
26
  };
16
27
  exports.fetchStatusRetry = fetchStatusRetry;
@@ -6,6 +6,14 @@
6
6
  * `const result = (array as Array<string | undefined>).filter(isDefined);`
7
7
  */
8
8
  export declare const isDefined: <X>(x: X) => x is Exclude<X, undefined>;
9
+ /**
10
+ * checks if a thing is not null. while often easy to do with a simple if statement,
11
+ * in certain situations the guard is required and its nice to have one pre-defined.
12
+ * E.g. in the following example, the result is `Array<string>`.
13
+ *
14
+ * `const result = (array as Array<string | null>).filter(isNotNull);`
15
+ */
16
+ export declare const isNotNull: <X>(x: X) => x is Exclude<X, null>;
9
17
  /**
10
18
  * checks if a thing is a number. while often easy to do with a simple if statement, in certain
11
19
  * situations the guard is required and its nice to have one pre-defined. E.g. in the following
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ifDefined = exports.isPlainObject = exports.isNumber = exports.isDefined = void 0;
3
+ exports.ifDefined = exports.isPlainObject = exports.isNumber = exports.isNotNull = exports.isDefined = void 0;
4
4
  /**
5
5
  * checks if a thing is defined. while often easy to do with a simple if statement, in certain
6
6
  * situations the guard is required and its nice to have one pre-defined. E.g. in the following
@@ -10,6 +10,15 @@ exports.ifDefined = exports.isPlainObject = exports.isNumber = exports.isDefined
10
10
  */
11
11
  const isDefined = (x) => x !== undefined;
12
12
  exports.isDefined = isDefined;
13
+ /**
14
+ * checks if a thing is not null. while often easy to do with a simple if statement,
15
+ * in certain situations the guard is required and its nice to have one pre-defined.
16
+ * E.g. in the following example, the result is `Array<string>`.
17
+ *
18
+ * `const result = (array as Array<string | null>).filter(isNotNull);`
19
+ */
20
+ const isNotNull = (x) => x !== null;
21
+ exports.isNotNull = isNotNull;
13
22
  /**
14
23
  * checks if a thing is a number. while often easy to do with a simple if statement, in certain
15
24
  * situations the guard is required and its nice to have one pre-defined. E.g. in the following
@@ -9,6 +9,7 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
9
9
  tableName: import("../../../config").ConfigValueProvider<string>;
10
10
  }; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
11
11
  afterWrite?: ((item: T) => void | Promise<void>) | undefined;
12
+ batchAfterWrite?: ((items: T[]) => void | Promise<void>) | undefined;
12
13
  } | undefined) => {
13
14
  loadAllDocumentsTheBadWay: () => Promise<T[]>;
14
15
  getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
@@ -20,5 +21,13 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
20
21
  incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
21
22
  patchItem: (item: Partial<T>) => Promise<T>;
22
23
  putItem: (item: T) => Promise<T>;
24
+ createItem: (item: T) => Promise<T>;
25
+ batchCreateItem: (items: T[], concurrency?: number) => Promise<{
26
+ successful: T[];
27
+ failed: {
28
+ item: T;
29
+ error: Error;
30
+ }[];
31
+ }>;
23
32
  };
24
33
  export {};
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.dynamoUnversionedDocumentStore = void 0;
4
7
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
8
+ const tiny_async_pool_1 = __importDefault(require("tiny-async-pool"));
5
9
  const __1 = require("../../..");
6
10
  const config_1 = require("../../../config");
7
11
  const errors_1 = require("../../../errors");
@@ -160,6 +164,69 @@ const dynamoUnversionedDocumentStore = (initializer) => {
160
164
  await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, updatedDoc));
161
165
  return updatedDoc;
162
166
  },
167
+ /* creates a new document, fails if the primary key already exists */
168
+ createItem: async (item) => {
169
+ const cmd = new client_dynamodb_1.PutItemCommand({
170
+ TableName: await tableName(),
171
+ Item: (0, dynamoEncoding_1.encodeDynamoDocument)(item),
172
+ ConditionExpression: 'attribute_not_exists(#k)',
173
+ ExpressionAttributeNames: {
174
+ '#k': hashKey.toString(),
175
+ },
176
+ });
177
+ return await dynamodb().send(cmd).then(() => item).catch((error) => {
178
+ throw error.name === 'ConditionalCheckFailedException' ?
179
+ new errors_1.ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`) : error;
180
+ }).then(async (createdDoc) => {
181
+ var _a;
182
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdDoc));
183
+ return createdDoc;
184
+ });
185
+ },
186
+ /* creates multiple new documents, returns successful and failed items separately */
187
+ batchCreateItem: async (items, concurrency = 10) => {
188
+ if (items.length === 0)
189
+ return { successful: [], failed: [] };
190
+ const successful = [];
191
+ const failed = [];
192
+ // Process using async iterator (tiny-async-pool v2)
193
+ for await (const result of (0, tiny_async_pool_1.default)(concurrency, items, async (item) => {
194
+ var _a;
195
+ try {
196
+ const cmd = new client_dynamodb_1.PutItemCommand({
197
+ TableName: await tableName(),
198
+ Item: (0, dynamoEncoding_1.encodeDynamoDocument)(item),
199
+ ConditionExpression: 'attribute_not_exists(#k)',
200
+ ExpressionAttributeNames: {
201
+ '#k': hashKey.toString(),
202
+ },
203
+ });
204
+ await dynamodb().send(cmd);
205
+ // Only call individual afterWrite if batchAfterWrite is not defined
206
+ if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
207
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
208
+ }
209
+ return { success: true, item };
210
+ }
211
+ catch (error) {
212
+ const processedError = error.name === 'ConditionalCheckFailedException' ?
213
+ new errors_1.ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`) : error;
214
+ return { success: false, item, error: processedError };
215
+ }
216
+ })) {
217
+ if (result.success) {
218
+ successful.push(result.item);
219
+ }
220
+ else {
221
+ failed.push({ item: result.item, error: result.error });
222
+ }
223
+ }
224
+ // Call batchAfterWrite with all successful items if defined
225
+ if ((options === null || options === void 0 ? void 0 : options.batchAfterWrite) && successful.length > 0) {
226
+ await options.batchAfterWrite(successful);
227
+ }
228
+ return { successful, failed };
229
+ },
163
230
  };
164
231
  };
165
232
  };
@@ -7,7 +7,10 @@ interface Initializer<C> {
7
7
  }
8
8
  export declare const fileSystemUnversionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends TDocument<T>>() => (configProvider: { [key in C]: {
9
9
  tableName: import("../../../config").ConfigValueProvider<string>;
10
- }; }) => <K extends keyof T>(_: {}, hashKey: K) => {
10
+ }; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
11
+ afterWrite?: ((item: T) => void | Promise<void>) | undefined;
12
+ batchAfterWrite?: ((items: T[]) => void | Promise<void>) | undefined;
13
+ } | undefined) => {
11
14
  loadAllDocumentsTheBadWay: () => Promise<T[]>;
12
15
  getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
13
16
  items: T[];
@@ -18,5 +21,13 @@ export declare const fileSystemUnversionedDocumentStore: <C extends string = "fi
18
21
  incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
19
22
  patchItem: (item: Partial<T>) => Promise<T>;
20
23
  putItem: (item: T) => Promise<T>;
24
+ createItem: (item: T) => Promise<T>;
25
+ batchCreateItem: (items: T[], _concurrency?: number) => Promise<{
26
+ successful: T[];
27
+ failed: {
28
+ item: T;
29
+ error: Error;
30
+ }[];
31
+ }>;
21
32
  };
22
33
  export {};
@@ -33,7 +33,7 @@ const __1 = require("../../..");
33
33
  const config_1 = require("../../../config");
34
34
  const errors_1 = require("../../../errors");
35
35
  const guards_1 = require("../../../guards");
36
- const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey) => {
36
+ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
37
37
  const tableName = (0, config_1.resolveConfigValue)(configProvider[initializer.configSpace || 'fileSystem'].tableName);
38
38
  const tablePath = tableName.then((table) => path_1.default.join(initializer.dataDir, table));
39
39
  const { mkdir, readdir, readFile, writeFile } = (0, guards_1.ifDefined)(initializer.fs, fsModule);
@@ -128,6 +128,54 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
128
128
  writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
129
129
  });
130
130
  },
131
+ createItem: async (item) => {
132
+ const hashed = hashFilename(item[hashKey]);
133
+ const existingItem = await load(hashed);
134
+ if (existingItem) {
135
+ throw new errors_1.ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`);
136
+ }
137
+ const path = await filePath(hashed);
138
+ await mkTableDir;
139
+ return new Promise((resolve, reject) => {
140
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
141
+ });
142
+ },
143
+ batchCreateItem: async (items, _concurrency = 1) => {
144
+ var _a;
145
+ if (items.length === 0)
146
+ return { successful: [], failed: [] };
147
+ const successful = [];
148
+ const failed = [];
149
+ // Process items sequentially to ensure consistent conflict detection
150
+ // Note: concurrency parameter is ignored for filesystem to avoid race conditions
151
+ for (const item of items) {
152
+ try {
153
+ const hashed = hashFilename(item[hashKey]);
154
+ const existingItem = await load(hashed);
155
+ if (existingItem) {
156
+ throw new errors_1.ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`);
157
+ }
158
+ const path = await filePath(hashed);
159
+ await mkTableDir;
160
+ const createdItem = await new Promise((resolve, reject) => {
161
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
162
+ });
163
+ successful.push(createdItem);
164
+ // Only call individual afterWrite if batchAfterWrite is not defined
165
+ if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
166
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdItem));
167
+ }
168
+ }
169
+ catch (error) {
170
+ failed.push({ item, error: error });
171
+ }
172
+ }
173
+ // Call batchAfterWrite with all successful items if defined
174
+ if ((options === null || options === void 0 ? void 0 : options.batchAfterWrite) && successful.length > 0) {
175
+ await options.batchAfterWrite(successful);
176
+ }
177
+ return { successful, failed };
178
+ },
131
179
  };
132
180
  };
133
181
  exports.fileSystemUnversionedDocumentStore = fileSystemUnversionedDocumentStore;
@@ -3,6 +3,7 @@ import type { JWK } from 'node-jose';
3
3
  import { ConfigProviderForConfig } from '../../config';
4
4
  declare type Config = {
5
5
  trustedDomain: string;
6
+ bypassSignatureVerification: string;
6
7
  };
7
8
  interface Initializer<C> {
8
9
  configSpace?: C;
@@ -15,6 +16,7 @@ interface Initializer<C> {
15
16
  */
16
17
  export declare const createLaunchVerifier: <C extends string = "launch">({ configSpace, fetcher }: Initializer<C>) => (configProvider: { [key in C]: {
17
18
  trustedDomain: import("../../config").ConfigValueProvider<string>;
19
+ bypassSignatureVerification: import("../../config").ConfigValueProvider<string>;
18
20
  }; }) => (_services: {}, getDefaultToken?: (() => string) | undefined) => {
19
21
  verify: <T = undefined>(...args: T extends undefined ? [] | [string] : [(input: any) => T] | [string, (input: any) => T]) => Promise<T extends undefined ? jwt.JwtPayload : T>;
20
22
  };
@@ -37,6 +37,7 @@ const helpers_1 = require("../../misc/helpers");
37
37
  const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvider) => {
38
38
  const config = configProvider[(0, guards_1.ifDefined)(configSpace, 'launch')];
39
39
  const getTrustedDomain = (0, helpers_1.once)(() => (0, config_1.resolveConfigValue)(config.trustedDomain));
40
+ const getBypassSignatureVerification = (0, helpers_1.once)(async () => (await (0, config_1.resolveConfigValue)(config.bypassSignatureVerification)) === 'true');
40
41
  const getJwksClient = (0, __1.memoize)((jwksUri) => new jwks_rsa_1.JwksClient({ fetcher, jwksUri }));
41
42
  const getJwksKey = (0, __1.memoize)(async (jwksUri, kid) => {
42
43
  const client = getJwksClient(jwksUri);
@@ -63,25 +64,26 @@ const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvider) => {
63
64
  }
64
65
  };
65
66
  return (_services, getDefaultToken) => {
66
- const verify = (...args) => {
67
+ const verify = async (...args) => {
67
68
  const [inputToken, validator] = args.length === 1
68
69
  ? typeof args[0] === 'string'
69
70
  ? [args[0], undefined]
70
71
  : [undefined, args[0]]
71
72
  : args;
73
+ const bypassSignatureVerification = await getBypassSignatureVerification();
72
74
  return new Promise((resolve, reject) => {
73
75
  const token = inputToken !== null && inputToken !== void 0 ? inputToken : getDefaultToken === null || getDefaultToken === void 0 ? void 0 : getDefaultToken();
74
76
  if (!token) {
75
77
  return reject(new errors_1.InvalidRequestError('Missing token for launch verification'));
76
78
  }
77
- return jsonwebtoken_1.default.verify(token, getKey, {}, (err, payload) => {
79
+ const resolvePayload = (err, payload) => {
78
80
  if (err && err instanceof jsonwebtoken_1.TokenExpiredError) {
79
81
  reject(new errors_1.SessionExpiredError());
80
82
  }
81
83
  else if (err) {
82
84
  reject(err);
83
85
  }
84
- else if (typeof payload !== 'object') {
86
+ else if (typeof payload !== 'object' || payload === null) {
85
87
  reject(new Error('received JWT token with unexpected non-JSON payload'));
86
88
  }
87
89
  else if (!payload.sub) {
@@ -102,7 +104,13 @@ const createLaunchVerifier = ({ configSpace, fetcher }) => (configProvider) => {
102
104
  // conditional return types are annoying
103
105
  resolve((validator ? validator(parsed) : parsed));
104
106
  }
105
- });
107
+ };
108
+ if (bypassSignatureVerification) {
109
+ return resolvePayload(null, jsonwebtoken_1.default.decode(token));
110
+ }
111
+ else {
112
+ return jsonwebtoken_1.default.verify(token, getKey, {}, resolvePayload);
113
+ }
106
114
  });
107
115
  };
108
116
  return { verify };
@@ -26,6 +26,9 @@ const openSearchService = (initializer = {}) => (configProvider) => {
26
26
  region: await (0, config_1.resolveConfigValue)(config.region),
27
27
  service: 'es',
28
28
  }),
29
+ maxRetries: 3,
30
+ requestTimeout: 3000,
31
+ pingTimeout: 1000,
29
32
  node: await (0, config_1.resolveConfigValue)(config.node),
30
33
  }));
31
34
  return (indexConfig) => {