@openstax/ts-utils 1.21.12 → 1.23.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 (35) 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/middleware/apiErrorHandler.d.ts +4 -3
  4. package/dist/cjs/middleware/apiErrorHandler.js +4 -3
  5. package/dist/cjs/misc/helpers.js +1 -1
  6. package/dist/cjs/services/accountsGateway/index.js +3 -4
  7. package/dist/cjs/services/apiGateway/index.d.ts +6 -2
  8. package/dist/cjs/services/apiGateway/index.js +17 -5
  9. package/dist/cjs/services/fileServer/index.d.ts +0 -2
  10. package/dist/cjs/services/fileServer/localFileServer.d.ts +0 -4
  11. package/dist/cjs/services/fileServer/localFileServer.js +0 -57
  12. package/dist/cjs/services/fileServer/s3FileServer.js +0 -24
  13. package/dist/cjs/services/lrsGateway/index.js +1 -1
  14. package/dist/cjs/services/postgresConnection/index.js +1 -3
  15. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
  16. package/dist/esm/errors/index.d.ts +11 -0
  17. package/dist/esm/errors/index.js +13 -0
  18. package/dist/esm/middleware/apiErrorHandler.d.ts +4 -3
  19. package/dist/esm/middleware/apiErrorHandler.js +5 -4
  20. package/dist/esm/misc/helpers.js +1 -1
  21. package/dist/esm/services/accountsGateway/index.js +3 -4
  22. package/dist/esm/services/apiGateway/index.d.ts +6 -2
  23. package/dist/esm/services/apiGateway/index.js +18 -6
  24. package/dist/esm/services/fileServer/index.d.ts +0 -2
  25. package/dist/esm/services/fileServer/localFileServer.d.ts +0 -4
  26. package/dist/esm/services/fileServer/localFileServer.js +0 -57
  27. package/dist/esm/services/fileServer/s3FileServer.js +1 -25
  28. package/dist/esm/services/lrsGateway/index.js +1 -1
  29. package/dist/esm/services/postgresConnection/index.js +1 -3
  30. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
  31. package/package.json +16 -16
  32. package/script/bin/deploy.bash +8 -0
  33. package/script/bin/get-env-param.bash +3 -3
  34. package/script/bin/init-params-script.bash +10 -1
  35. package/script/bin/upload-params.bash +3 -3
@@ -42,6 +42,17 @@ export declare class UnauthorizedError extends Error {
42
42
  static matches: (e: any) => e is typeof UnauthorizedError;
43
43
  constructor(message?: string);
44
44
  }
45
+ /**
46
+ * Forbidden error
47
+ *
48
+ * `ForbiddenError.matches(error)` is a reliable way to check if an error is a
49
+ * `ForbiddenError`; `instanceof` checks may not work if code is split into multiple bundles
50
+ */
51
+ export declare class ForbiddenError extends Error {
52
+ static readonly TYPE = "ForbiddenError";
53
+ static matches: (e: any) => e is typeof ForbiddenError;
54
+ constructor(message?: string);
55
+ }
45
56
  /**
46
57
  * Not found error
47
58
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SessionExpiredError = exports.NotFoundError = exports.UnauthorizedError = exports.ValidationError = exports.InvalidRequestError = exports.isAppError = void 0;
3
+ 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
@@ -65,6 +65,20 @@ class UnauthorizedError extends Error {
65
65
  exports.UnauthorizedError = UnauthorizedError;
66
66
  UnauthorizedError.TYPE = 'UnauthorizedError';
67
67
  UnauthorizedError.matches = errorIsType(UnauthorizedError);
68
+ /**
69
+ * Forbidden error
70
+ *
71
+ * `ForbiddenError.matches(error)` is a reliable way to check if an error is a
72
+ * `ForbiddenError`; `instanceof` checks may not work if code is split into multiple bundles
73
+ */
74
+ class ForbiddenError extends Error {
75
+ constructor(message) {
76
+ super(message || ForbiddenError.TYPE);
77
+ }
78
+ }
79
+ exports.ForbiddenError = ForbiddenError;
80
+ ForbiddenError.TYPE = 'ForbiddenError';
81
+ ForbiddenError.matches = errorIsType(ForbiddenError);
68
82
  /**
69
83
  * Not found error
70
84
  *
@@ -1,12 +1,13 @@
1
- import { InvalidRequestError, NotFoundError, SessionExpiredError, UnauthorizedError, ValidationError } from '../errors';
1
+ import { ForbiddenError, InvalidRequestError, NotFoundError, SessionExpiredError, UnauthorizedError, ValidationError } from '../errors';
2
2
  import type { ApiResponse } from '../routing';
3
3
  import type { Logger } from '../services/logger';
4
4
  export declare type DefaultErrors = {
5
+ InvalidRequestError: InvalidRequestError;
5
6
  UnauthorizedError: UnauthorizedError;
6
- SessionExpiredError: SessionExpiredError;
7
+ ForbiddenError: ForbiddenError;
7
8
  NotFoundError: NotFoundError;
8
- InvalidRequestError: InvalidRequestError;
9
9
  ValidationError: ValidationError;
10
+ SessionExpiredError: SessionExpiredError;
10
11
  };
11
12
  export declare type Handlers<E> = {
12
13
  [T in keyof E]: (e: E[T], logger: Logger) => ApiResponse<number, any>;
@@ -5,11 +5,12 @@ const errors_1 = require("../errors");
5
5
  const routing_1 = require("../routing");
6
6
  const logger_1 = require("../services/logger");
7
7
  exports.defaultHandlers = {
8
- UnauthorizedError: () => (0, routing_1.apiTextResponse)(401, '401 UnauthorizedError'),
9
- SessionExpiredError: () => (0, routing_1.apiTextResponse)(440, '440 SessionExpiredError'),
10
- NotFoundError: (e) => (0, routing_1.apiTextResponse)(404, `404 ${e.message}`),
11
8
  InvalidRequestError: (e) => (0, routing_1.apiTextResponse)(400, `400 ${e.message}`),
9
+ UnauthorizedError: (e) => (0, routing_1.apiTextResponse)(401, `401 ${e.message}`),
10
+ ForbiddenError: (e) => (0, routing_1.apiTextResponse)(403, `403 ${e.message}`),
11
+ NotFoundError: (e) => (0, routing_1.apiTextResponse)(404, `404 ${e.message}`),
12
12
  ValidationError: (e) => (0, routing_1.apiJsonResponse)(422, e.getData()),
13
+ SessionExpiredError: (e) => (0, routing_1.apiTextResponse)(440, `440 ${e.message}`),
13
14
  };
14
15
  /**
15
16
  * Creates an error handler. Provides default handlers for `UnauthorizedError`,
@@ -84,7 +84,7 @@ const retryWithDelay = (fn, options) => {
84
84
  reject(e);
85
85
  }
86
86
  else {
87
- (_a = options === null || options === void 0 ? void 0 : options.logger) === null || _a === void 0 ? void 0 : _a.log(`failed try ${n} of ${retries}. ${e.message}`);
87
+ (_a = options === null || options === void 0 ? void 0 : options.logger) === null || _a === void 0 ? void 0 : _a.log(`failed try ${n + 1} of ${retries}. ${e.message}`);
88
88
  setTimeout(() => (0, exports.retryWithDelay)(fn, { ...options, n: n + 1 }).then(resolve, reject), timeout);
89
89
  }
90
90
  });
@@ -23,11 +23,10 @@ const accountsGateway = (initializer) => (configProvider) => {
23
23
  const request = async (method, path, options, statuses = [200, 201]) => {
24
24
  const host = (await accountsBase).replace(/\/+$/, '');
25
25
  const url = `${host}/api/${path}`;
26
- const token = options.token || await accountsAuthToken;
27
26
  const config = {
28
- headers: token
29
- ? { Authorization: `Bearer ${token}` }
30
- : {},
27
+ headers: {
28
+ Authorization: `Bearer ${options.token || await accountsAuthToken}`,
29
+ },
31
30
  method,
32
31
  };
33
32
  if (options.body) {
@@ -3,6 +3,7 @@ import { ConfigProviderForConfig } from '../../config';
3
3
  import { ConfigForFetch, GenericFetch, Response } from '../../fetch';
4
4
  import { AnyRoute, ApiResponse, OutputForRoute, ParamsForRoute, PayloadForRoute, QueryParams } from '../../routing';
5
5
  import { UnwrapPromise } from '../../types';
6
+ import { Logger } from '../logger';
6
7
  declare type TResponsePayload<R> = R extends ApiResponse<any, infer P> ? P : never;
7
8
  declare type TResponseStatus<R> = R extends ApiResponse<infer S, any> ? S : never;
8
9
  declare type RouteClient<R> = {
@@ -49,8 +50,11 @@ export declare const loadResponse: (response: Response) => () => Promise<any>;
49
50
  interface MakeApiGateway<F> {
50
51
  <Ru>(config: ConfigProviderForConfig<{
51
52
  apiBase: string;
52
- }>, routes: MapRoutesToConfig<Ru>, authProvider?: {
53
- getAuthorizedFetchConfig: () => Promise<ConfigForFetch<F>>;
53
+ }>, routes: MapRoutesToConfig<Ru>, appProvider?: {
54
+ authProvider?: {
55
+ getAuthorizedFetchConfig: () => Promise<ConfigForFetch<F>>;
56
+ };
57
+ logger?: Logger;
54
58
  }): MapRoutesToClient<Ru>;
55
59
  }
56
60
  export declare const createApiGateway: <F extends GenericFetch<import("../../fetch").FetchConfig, Response>>(initializer: {
@@ -29,10 +29,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.createApiGateway = exports.loadResponse = void 0;
30
30
  const pathToRegexp = __importStar(require("path-to-regexp"));
31
31
  const query_string_1 = __importDefault(require("query-string"));
32
+ const uuid_1 = require("uuid");
32
33
  const __1 = require("../..");
33
34
  const config_1 = require("../../config");
34
35
  const errors_1 = require("../../errors");
35
36
  const fetchStatusRetry_1 = require("../../fetch/fetchStatusRetry");
37
+ const logger_1 = require("../logger");
36
38
  /** Pulls the content out of a response based on the content type */
37
39
  const loadResponse = (response) => () => {
38
40
  const [contentType] = (response.headers.get('content-type') || '').split(';');
@@ -46,7 +48,7 @@ const loadResponse = (response) => () => {
46
48
  }
47
49
  };
48
50
  exports.loadResponse = loadResponse;
49
- const makeRouteClient = (initializer, config, route, authProvider) => {
51
+ const makeRouteClient = (initializer, config, route, appProvider) => {
50
52
  /* TODO this duplicates code with makeRenderRouteUrl, reuse that */
51
53
  const renderUrl = async ({ params, query }) => {
52
54
  const apiBase = await (0, config_1.resolveConfigValue)(config.apiBase);
@@ -55,21 +57,31 @@ const makeRouteClient = (initializer, config, route, authProvider) => {
55
57
  return apiBase.replace(/\/+$/, '') + getPathForParams(params || {}) + (search ? `?${search}` : '');
56
58
  };
57
59
  const routeClient = async ({ params, payload, query, fetchConfig }) => {
60
+ var _a, _b;
61
+ const { fetch } = initializer;
58
62
  const url = await renderUrl({ params, query });
59
63
  const body = payload ? JSON.stringify(payload) : undefined;
60
- const baseOptions = (0, __1.merge)((await (authProvider === null || authProvider === void 0 ? void 0 : authProvider.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
61
- const fetcher = (0, fetchStatusRetry_1.fetchStatusRetry)(initializer.fetch, { retries: 1, status: [502] });
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 || {});
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();
67
+ requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.setContext({ requestId, url, timeStamp: new Date().getTime() });
68
+ const fetcher = (0, fetchStatusRetry_1.fetchStatusRetry)(fetch, { retries: 1, status: [502], logger: requestLogger });
69
+ requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.log('Request Initiated', logger_1.Level.Info);
62
70
  return fetcher(url, (0, __1.merge)(baseOptions, {
63
71
  method: route.method,
64
72
  body,
65
73
  headers: {
66
74
  ...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
67
75
  ...(body ? { 'content-type': 'application/json' } : {}),
76
+ 'X-Request-ID': requestId,
68
77
  }
69
78
  })).then(response => {
70
79
  if (response.status === 401) {
71
80
  throw new errors_1.UnauthorizedError();
72
81
  }
82
+ if (response.status === 403) {
83
+ throw new errors_1.ForbiddenError();
84
+ }
73
85
  if (response.status === 440) {
74
86
  throw new errors_1.SessionExpiredError();
75
87
  }
@@ -89,8 +101,8 @@ const makeRouteClient = (initializer, config, route, authProvider) => {
89
101
  routeClient.renderUrl = renderUrl;
90
102
  return routeClient;
91
103
  };
92
- const createApiGateway = (initializer) => (config, routes, authProvider) => {
104
+ const createApiGateway = (initializer) => (config, routes, appProvider) => {
93
105
  return Object.fromEntries(Object.entries(routes)
94
- .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, authProvider)])));
106
+ .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, appProvider)])));
95
107
  };
96
108
  exports.createApiGateway = createApiGateway;
@@ -12,8 +12,6 @@ export declare type FolderValue = {
12
12
  export declare const isFileValue: (thing: any) => thing is FileValue;
13
13
  export declare const isFolderValue: (thing: any) => thing is FolderValue;
14
14
  export interface FileServerAdapter {
15
- putFileContent: (source: FileValue, content: string) => Promise<FileValue>;
16
- getSignedViewerUrl: (source: FileValue) => Promise<string>;
17
15
  getFileContent: (source: FileValue) => Promise<Buffer>;
18
16
  }
19
17
  export declare const isFileOrFolder: (thing: any) => thing is FileValue | FolderValue;
@@ -1,8 +1,6 @@
1
1
  import { ConfigProviderForConfig } from '../../config';
2
2
  import { FileServerAdapter } from '.';
3
3
  export declare type Config = {
4
- port?: string;
5
- host?: string;
6
4
  storagePrefix: string;
7
5
  };
8
6
  interface Initializer<C> {
@@ -10,8 +8,6 @@ interface Initializer<C> {
10
8
  configSpace?: C;
11
9
  }
12
10
  export declare const localFileServer: <C extends string = "local">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
13
- port?: import("../../config").ConfigValueProvider<string> | undefined;
14
- host?: import("../../config").ConfigValueProvider<string> | undefined;
15
11
  storagePrefix: import("../../config").ConfigValueProvider<string>;
16
12
  }; }) => FileServerAdapter;
17
13
  export {};
@@ -8,73 +8,16 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("../../config");
10
10
  const guards_1 = require("../../guards");
11
- const cors_1 = __importDefault(require("cors"));
12
- const express_1 = __importDefault(require("express"));
13
- const multer_1 = __importDefault(require("multer"));
14
- const https_1 = __importDefault(require("https"));
15
- const helpers_1 = require("../../misc/helpers");
16
- const assertions_1 = require("../../assertions");
17
- /* istanbul ignore next */
18
- const startServer = (0, helpers_1.once)((port, uploadDir) => {
19
- // TODO - re-evaluate the `preservePath` behavior to match whatever s3 does
20
- const upload = (0, multer_1.default)({ dest: uploadDir, preservePath: true });
21
- const fileServerApp = (0, express_1.default)();
22
- fileServerApp.use((0, cors_1.default)());
23
- fileServerApp.use(express_1.default.static(uploadDir));
24
- fileServerApp.post('/', upload.single('file'), async (req, res) => {
25
- const file = req.file;
26
- if (!file) {
27
- return res.status(400).send({ message: 'file is required' });
28
- }
29
- const destinationName = req.body.key.replace('${filename}', file.originalname);
30
- const destinationPath = path_1.default.join(uploadDir, destinationName);
31
- const destinationDirectory = path_1.default.dirname(destinationPath);
32
- await fs_1.default.promises.mkdir(destinationDirectory, { recursive: true });
33
- await fs_1.default.promises.rename(file.path, destinationPath);
34
- res.status(201).send();
35
- });
36
- const server = https_1.default.createServer({
37
- key: fs_1.default.readFileSync((0, assertions_1.assertString)(process.env.SSL_KEY_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
38
- cert: fs_1.default.readFileSync((0, assertions_1.assertString)(process.env.SSL_CRT_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
39
- }, fileServerApp);
40
- server.once('error', function (err) {
41
- if (err.code === 'EADDRINUSE') {
42
- // when the local dev server reloads files on every request it doesn't
43
- // actually tear down the old modules, so this server only starts on the
44
- // first execution and changes in its code will not be reloaded
45
- return;
46
- }
47
- throw err;
48
- });
49
- server.listen(port);
50
- return true;
51
- });
52
11
  const localFileServer = (initializer) => (configProvider) => {
53
12
  const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'local')];
54
- const port = (0, config_1.resolveConfigValue)(config.port || '');
55
- const host = (0, config_1.resolveConfigValue)(config.host || '');
56
13
  const storagePrefix = (0, config_1.resolveConfigValue)(config.storagePrefix);
57
14
  const fileDir = storagePrefix.then((prefix) => path_1.default.join(initializer.dataDir, prefix));
58
- Promise.all([port, fileDir])
59
- .then(([port, fileDir]) => port && startServer(port, fileDir));
60
- const getSignedViewerUrl = async (source) => {
61
- return `https://${await host}:${await port}/${source.path}`;
62
- };
63
15
  const getFileContent = async (source) => {
64
16
  const filePath = path_1.default.join(await fileDir, source.path);
65
17
  return fs_1.default.promises.readFile(filePath);
66
18
  };
67
- const putFileContent = async (source, content) => {
68
- const filePath = path_1.default.join(await fileDir, source.path);
69
- const directory = path_1.default.dirname(filePath);
70
- await fs_1.default.promises.mkdir(directory, { recursive: true });
71
- await fs_1.default.promises.writeFile(filePath, content);
72
- return source;
73
- };
74
19
  return {
75
- getSignedViewerUrl,
76
20
  getFileContent,
77
- putFileContent,
78
21
  };
79
22
  };
80
23
  exports.localFileServer = localFileServer;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.s3FileServer = void 0;
4
4
  const client_s3_1 = require("@aws-sdk/client-s3");
5
- const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
6
5
  const __1 = require("../..");
7
6
  const assertions_1 = require("../../assertions");
8
7
  const config_1 = require("../../config");
@@ -13,37 +12,14 @@ const s3FileServer = (initializer) => (configProvider) => {
13
12
  const bucketRegion = (0, __1.once)(() => (0, config_1.resolveConfigValue)(config.bucketRegion));
14
13
  const client = (0, guards_1.ifDefined)(initializer.s3Client, client_s3_1.S3Client);
15
14
  const s3Service = (0, __1.once)(async () => new client({ apiVersion: '2012-08-10', region: await bucketRegion() }));
16
- /*
17
- * https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
18
- */
19
- const getSignedViewerUrl = async (source) => {
20
- const bucket = (await bucketName());
21
- const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: source.path });
22
- return (0, s3_request_presigner_1.getSignedUrl)(await s3Service(), command, {
23
- expiresIn: 3600, // 1 hour
24
- });
25
- };
26
15
  const getFileContent = async (source) => {
27
16
  const bucket = await bucketName();
28
17
  const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: source.path });
29
18
  const response = await (await s3Service()).send(command);
30
19
  return Buffer.from(await (0, assertions_1.assertDefined)(response.Body, new Error('Invalid Response from s3')).transformToByteArray());
31
20
  };
32
- const putFileContent = async (source, content) => {
33
- const bucket = await bucketName();
34
- const command = new client_s3_1.PutObjectCommand({
35
- Bucket: bucket,
36
- Key: source.path,
37
- Body: content,
38
- ContentType: source.mimeType,
39
- });
40
- await (await s3Service()).send(command);
41
- return source;
42
- };
43
21
  return {
44
22
  getFileContent,
45
- putFileContent,
46
- getSignedViewerUrl,
47
23
  };
48
24
  };
49
25
  exports.s3FileServer = s3FileServer;
@@ -108,7 +108,7 @@ ${await response.text()}`);
108
108
  const response = await fetchXapiStatements(fetchParams).catch(abort);
109
109
  const consistentThrough = response.headers.get('X-Experience-API-Consistent-Through');
110
110
  if (!consistentThrough || new Date(consistentThrough) < date) {
111
- throw new Error(`xAPI consistent through ${consistentThrough}; not in sync with current date ${date}.`);
111
+ throw new Error(`xAPI consistent through ${consistentThrough}; not in sync with current date ${date.toISOString()}.`);
112
112
  }
113
113
  return formatGetXapiStatementsResponse(response);
114
114
  }, { retries: 4, logger });
@@ -18,9 +18,7 @@ const postgresConnection = (initializer) => (configProvider) => {
18
18
  database: await (0, config_1.resolveConfigValue)(config.database),
19
19
  username: await (0, config_1.resolveConfigValue)(config.username),
20
20
  password: await (0, config_1.resolveConfigValue)(config.password),
21
- transform: {
22
- column: { to: postgres_1.default.fromCamel, from: postgres_1.default.toCamel },
23
- },
21
+ transform: postgres_1.default.camel,
24
22
  }));
25
23
  const connections = [];
26
24
  const sql = (0, helpers_1.once)(async () => {