@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
  *
@@ -58,6 +58,19 @@ export class UnauthorizedError extends Error {
58
58
  }
59
59
  UnauthorizedError.TYPE = 'UnauthorizedError';
60
60
  UnauthorizedError.matches = errorIsType(UnauthorizedError);
61
+ /**
62
+ * Forbidden error
63
+ *
64
+ * `ForbiddenError.matches(error)` is a reliable way to check if an error is a
65
+ * `ForbiddenError`; `instanceof` checks may not work if code is split into multiple bundles
66
+ */
67
+ export class ForbiddenError extends Error {
68
+ constructor(message) {
69
+ super(message || ForbiddenError.TYPE);
70
+ }
71
+ }
72
+ ForbiddenError.TYPE = 'ForbiddenError';
73
+ ForbiddenError.matches = errorIsType(ForbiddenError);
61
74
  /**
62
75
  * Not found error
63
76
  *
@@ -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>;
@@ -1,12 +1,13 @@
1
- import { isAppError } from '../errors';
1
+ import { isAppError, } from '../errors';
2
2
  import { apiJsonResponse, apiTextResponse } from '../routing';
3
3
  import { Level } from '../services/logger';
4
4
  export const defaultHandlers = {
5
- UnauthorizedError: () => apiTextResponse(401, '401 UnauthorizedError'),
6
- SessionExpiredError: () => apiTextResponse(440, '440 SessionExpiredError'),
7
- NotFoundError: (e) => apiTextResponse(404, `404 ${e.message}`),
8
5
  InvalidRequestError: (e) => apiTextResponse(400, `400 ${e.message}`),
6
+ UnauthorizedError: (e) => apiTextResponse(401, `401 ${e.message}`),
7
+ ForbiddenError: (e) => apiTextResponse(403, `403 ${e.message}`),
8
+ NotFoundError: (e) => apiTextResponse(404, `404 ${e.message}`),
9
9
  ValidationError: (e) => apiJsonResponse(422, e.getData()),
10
+ SessionExpiredError: (e) => apiTextResponse(440, `440 ${e.message}`),
10
11
  };
11
12
  /**
12
13
  * Creates an error handler. Provides default handlers for `UnauthorizedError`,
@@ -76,7 +76,7 @@ export const retryWithDelay = (fn, options) => {
76
76
  reject(e);
77
77
  }
78
78
  else {
79
- (_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}`);
79
+ (_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}`);
80
80
  setTimeout(() => retryWithDelay(fn, { ...options, n: n + 1 }).then(resolve, reject), timeout);
81
81
  }
82
82
  });
@@ -17,11 +17,10 @@ export const accountsGateway = (initializer) => (configProvider) => {
17
17
  const request = async (method, path, options, statuses = [200, 201]) => {
18
18
  const host = (await accountsBase).replace(/\/+$/, '');
19
19
  const url = `${host}/api/${path}`;
20
- const token = options.token || await accountsAuthToken;
21
20
  const config = {
22
- headers: token
23
- ? { Authorization: `Bearer ${token}` }
24
- : {},
21
+ headers: {
22
+ Authorization: `Bearer ${options.token || await accountsAuthToken}`,
23
+ },
25
24
  method,
26
25
  };
27
26
  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: {
@@ -1,9 +1,11 @@
1
1
  import * as pathToRegexp from 'path-to-regexp';
2
2
  import queryString from 'query-string';
3
+ import { v4 as uuid } from 'uuid';
3
4
  import { merge } from '../..';
4
5
  import { resolveConfigValue } from '../../config';
5
- import { SessionExpiredError, UnauthorizedError } from '../../errors';
6
+ import { ForbiddenError, SessionExpiredError, UnauthorizedError } from '../../errors';
6
7
  import { fetchStatusRetry } from '../../fetch/fetchStatusRetry';
8
+ import { Level } from '../logger';
7
9
  /** Pulls the content out of a response based on the content type */
8
10
  export const loadResponse = (response) => () => {
9
11
  const [contentType] = (response.headers.get('content-type') || '').split(';');
@@ -16,7 +18,7 @@ export const loadResponse = (response) => () => {
16
18
  throw new Error(`unknown content type ${contentType}`);
17
19
  }
18
20
  };
19
- const makeRouteClient = (initializer, config, route, authProvider) => {
21
+ const makeRouteClient = (initializer, config, route, appProvider) => {
20
22
  /* TODO this duplicates code with makeRenderRouteUrl, reuse that */
21
23
  const renderUrl = async ({ params, query }) => {
22
24
  const apiBase = await resolveConfigValue(config.apiBase);
@@ -25,21 +27,31 @@ const makeRouteClient = (initializer, config, route, authProvider) => {
25
27
  return apiBase.replace(/\/+$/, '') + getPathForParams(params || {}) + (search ? `?${search}` : '');
26
28
  };
27
29
  const routeClient = async ({ params, payload, query, fetchConfig }) => {
30
+ var _a, _b;
31
+ const { fetch } = initializer;
28
32
  const url = await renderUrl({ params, query });
29
33
  const body = payload ? JSON.stringify(payload) : undefined;
30
- const baseOptions = merge((await (authProvider === null || authProvider === void 0 ? void 0 : authProvider.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
31
- const fetcher = fetchStatusRetry(initializer.fetch, { retries: 1, status: [502] });
34
+ const baseOptions = merge((await ((_a = appProvider === null || appProvider === void 0 ? void 0 : appProvider.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
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();
37
+ requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.setContext({ requestId, url, timeStamp: new Date().getTime() });
38
+ const fetcher = fetchStatusRetry(fetch, { retries: 1, status: [502], logger: requestLogger });
39
+ requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.log('Request Initiated', Level.Info);
32
40
  return fetcher(url, merge(baseOptions, {
33
41
  method: route.method,
34
42
  body,
35
43
  headers: {
36
44
  ...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
37
45
  ...(body ? { 'content-type': 'application/json' } : {}),
46
+ 'X-Request-ID': requestId,
38
47
  }
39
48
  })).then(response => {
40
49
  if (response.status === 401) {
41
50
  throw new UnauthorizedError();
42
51
  }
52
+ if (response.status === 403) {
53
+ throw new ForbiddenError();
54
+ }
43
55
  if (response.status === 440) {
44
56
  throw new SessionExpiredError();
45
57
  }
@@ -59,7 +71,7 @@ const makeRouteClient = (initializer, config, route, authProvider) => {
59
71
  routeClient.renderUrl = renderUrl;
60
72
  return routeClient;
61
73
  };
62
- export const createApiGateway = (initializer) => (config, routes, authProvider) => {
74
+ export const createApiGateway = (initializer) => (config, routes, appProvider) => {
63
75
  return Object.fromEntries(Object.entries(routes)
64
- .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, authProvider)])));
76
+ .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, appProvider)])));
65
77
  };
@@ -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 {};
@@ -2,72 +2,15 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolveConfigValue } from '../../config';
4
4
  import { ifDefined } from '../../guards';
5
- import cors from 'cors';
6
- import express from 'express';
7
- import multer from 'multer';
8
- import https from 'https';
9
- import { once } from "../../misc/helpers";
10
- import { assertString } from "../../assertions";
11
- /* istanbul ignore next */
12
- const startServer = once((port, uploadDir) => {
13
- // TODO - re-evaluate the `preservePath` behavior to match whatever s3 does
14
- const upload = multer({ dest: uploadDir, preservePath: true });
15
- const fileServerApp = express();
16
- fileServerApp.use(cors());
17
- fileServerApp.use(express.static(uploadDir));
18
- fileServerApp.post('/', upload.single('file'), async (req, res) => {
19
- const file = req.file;
20
- if (!file) {
21
- return res.status(400).send({ message: 'file is required' });
22
- }
23
- const destinationName = req.body.key.replace('${filename}', file.originalname);
24
- const destinationPath = path.join(uploadDir, destinationName);
25
- const destinationDirectory = path.dirname(destinationPath);
26
- await fs.promises.mkdir(destinationDirectory, { recursive: true });
27
- await fs.promises.rename(file.path, destinationPath);
28
- res.status(201).send();
29
- });
30
- const server = https.createServer({
31
- key: fs.readFileSync(assertString(process.env.SSL_KEY_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
32
- cert: fs.readFileSync(assertString(process.env.SSL_CRT_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
33
- }, fileServerApp);
34
- server.once('error', function (err) {
35
- if (err.code === 'EADDRINUSE') {
36
- // when the local dev server reloads files on every request it doesn't
37
- // actually tear down the old modules, so this server only starts on the
38
- // first execution and changes in its code will not be reloaded
39
- return;
40
- }
41
- throw err;
42
- });
43
- server.listen(port);
44
- return true;
45
- });
46
5
  export const localFileServer = (initializer) => (configProvider) => {
47
6
  const config = configProvider[ifDefined(initializer.configSpace, 'local')];
48
- const port = resolveConfigValue(config.port || '');
49
- const host = resolveConfigValue(config.host || '');
50
7
  const storagePrefix = resolveConfigValue(config.storagePrefix);
51
8
  const fileDir = storagePrefix.then((prefix) => path.join(initializer.dataDir, prefix));
52
- Promise.all([port, fileDir])
53
- .then(([port, fileDir]) => port && startServer(port, fileDir));
54
- const getSignedViewerUrl = async (source) => {
55
- return `https://${await host}:${await port}/${source.path}`;
56
- };
57
9
  const getFileContent = async (source) => {
58
10
  const filePath = path.join(await fileDir, source.path);
59
11
  return fs.promises.readFile(filePath);
60
12
  };
61
- const putFileContent = async (source, content) => {
62
- const filePath = path.join(await fileDir, source.path);
63
- const directory = path.dirname(filePath);
64
- await fs.promises.mkdir(directory, { recursive: true });
65
- await fs.promises.writeFile(filePath, content);
66
- return source;
67
- };
68
13
  return {
69
- getSignedViewerUrl,
70
14
  getFileContent,
71
- putFileContent,
72
15
  };
73
16
  };
@@ -1,5 +1,4 @@
1
- import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
2
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
1
+ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
2
  import { once } from '../..';
4
3
  import { assertDefined } from '../../assertions';
5
4
  import { resolveConfigValue } from '../../config';
@@ -10,36 +9,13 @@ export const s3FileServer = (initializer) => (configProvider) => {
10
9
  const bucketRegion = once(() => resolveConfigValue(config.bucketRegion));
11
10
  const client = ifDefined(initializer.s3Client, S3Client);
12
11
  const s3Service = once(async () => new client({ apiVersion: '2012-08-10', region: await bucketRegion() }));
13
- /*
14
- * https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
15
- */
16
- const getSignedViewerUrl = async (source) => {
17
- const bucket = (await bucketName());
18
- const command = new GetObjectCommand({ Bucket: bucket, Key: source.path });
19
- return getSignedUrl(await s3Service(), command, {
20
- expiresIn: 3600, // 1 hour
21
- });
22
- };
23
12
  const getFileContent = async (source) => {
24
13
  const bucket = await bucketName();
25
14
  const command = new GetObjectCommand({ Bucket: bucket, Key: source.path });
26
15
  const response = await (await s3Service()).send(command);
27
16
  return Buffer.from(await assertDefined(response.Body, new Error('Invalid Response from s3')).transformToByteArray());
28
17
  };
29
- const putFileContent = async (source, content) => {
30
- const bucket = await bucketName();
31
- const command = new PutObjectCommand({
32
- Bucket: bucket,
33
- Key: source.path,
34
- Body: content,
35
- ContentType: source.mimeType,
36
- });
37
- await (await s3Service()).send(command);
38
- return source;
39
- };
40
18
  return {
41
19
  getFileContent,
42
- putFileContent,
43
- getSignedViewerUrl,
44
20
  };
45
21
  };
@@ -82,7 +82,7 @@ ${await response.text()}`);
82
82
  const response = await fetchXapiStatements(fetchParams).catch(abort);
83
83
  const consistentThrough = response.headers.get('X-Experience-API-Consistent-Through');
84
84
  if (!consistentThrough || new Date(consistentThrough) < date) {
85
- throw new Error(`xAPI consistent through ${consistentThrough}; not in sync with current date ${date}.`);
85
+ throw new Error(`xAPI consistent through ${consistentThrough}; not in sync with current date ${date.toISOString()}.`);
86
86
  }
87
87
  return formatGetXapiStatementsResponse(response);
88
88
  }, { retries: 4, logger });
@@ -12,9 +12,7 @@ export const postgresConnection = (initializer) => (configProvider) => {
12
12
  database: await resolveConfigValue(config.database),
13
13
  username: await resolveConfigValue(config.username),
14
14
  password: await resolveConfigValue(config.password),
15
- transform: {
16
- column: { to: postgres.fromCamel, from: postgres.toCamel },
17
- },
15
+ transform: postgres.camel,
18
16
  }));
19
17
  const connections = [];
20
18
  const sql = once(async () => {