@openstax/ts-utils 1.23.0 → 1.24.1

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.
@@ -12,6 +12,8 @@ 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>;
15
17
  getFileContent: (source: FileValue) => Promise<Buffer>;
16
18
  }
17
19
  export declare const isFileOrFolder: (thing: any) => thing is FileValue | FolderValue;
@@ -1,6 +1,8 @@
1
1
  import { ConfigProviderForConfig } from '../../config';
2
2
  import { FileServerAdapter } from '.';
3
3
  export declare type Config = {
4
+ port?: string;
5
+ host?: string;
4
6
  storagePrefix: string;
5
7
  };
6
8
  interface Initializer<C> {
@@ -8,6 +10,8 @@ interface Initializer<C> {
8
10
  configSpace?: C;
9
11
  }
10
12
  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;
11
15
  storagePrefix: import("../../config").ConfigValueProvider<string>;
12
16
  }; }) => FileServerAdapter;
13
17
  export {};
@@ -4,20 +4,78 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.localFileServer = void 0;
7
+ /* cspell:ignore originalname */
7
8
  const fs_1 = __importDefault(require("fs"));
9
+ const https_1 = __importDefault(require("https"));
8
10
  const path_1 = __importDefault(require("path"));
11
+ const cors_1 = __importDefault(require("cors"));
12
+ const express_1 = __importDefault(require("express"));
13
+ const multer_1 = __importDefault(require("multer"));
14
+ const assertions_1 = require("../../assertions");
9
15
  const config_1 = require("../../config");
10
16
  const guards_1 = require("../../guards");
17
+ const helpers_1 = require("../../misc/helpers");
18
+ /* istanbul ignore next */
19
+ const startServer = (0, helpers_1.once)((port, uploadDir) => {
20
+ // TODO - re-evaluate the `preservePath` behavior to match whatever s3 does
21
+ const upload = (0, multer_1.default)({ dest: uploadDir, preservePath: true });
22
+ const fileServerApp = (0, express_1.default)();
23
+ fileServerApp.use((0, cors_1.default)());
24
+ fileServerApp.use(express_1.default.static(uploadDir));
25
+ fileServerApp.post('/', upload.single('file'), async (req, res) => {
26
+ const file = req.file;
27
+ if (!file) {
28
+ return res.status(400).send({ message: 'file is required' });
29
+ }
30
+ const destinationName = req.body.key.replace('${filename}', file.originalname);
31
+ const destinationPath = path_1.default.join(uploadDir, destinationName);
32
+ const destinationDirectory = path_1.default.dirname(destinationPath);
33
+ await fs_1.default.promises.mkdir(destinationDirectory, { recursive: true });
34
+ await fs_1.default.promises.rename(file.path, destinationPath);
35
+ res.status(201).send();
36
+ });
37
+ const server = https_1.default.createServer({
38
+ key: fs_1.default.readFileSync((0, assertions_1.assertString)(process.env.SSL_KEY_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
39
+ cert: fs_1.default.readFileSync((0, assertions_1.assertString)(process.env.SSL_CRT_FILE, new Error('ssl key is required for localFileServer')), 'utf8'),
40
+ }, fileServerApp);
41
+ server.once('error', function (err) {
42
+ if (err.code === 'EADDRINUSE') {
43
+ // when the local dev server reloads files on every request it doesn't
44
+ // actually tear down the old modules, so this server only starts on the
45
+ // first execution and changes in its code will not be reloaded
46
+ return;
47
+ }
48
+ throw err;
49
+ });
50
+ server.listen(port);
51
+ return true;
52
+ });
11
53
  const localFileServer = (initializer) => (configProvider) => {
12
54
  const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'local')];
55
+ const port = (0, config_1.resolveConfigValue)(config.port || '');
56
+ const host = (0, config_1.resolveConfigValue)(config.host || '');
13
57
  const storagePrefix = (0, config_1.resolveConfigValue)(config.storagePrefix);
14
58
  const fileDir = storagePrefix.then((prefix) => path_1.default.join(initializer.dataDir, prefix));
59
+ Promise.all([port, fileDir])
60
+ .then(([port, fileDir]) => port && startServer(port, fileDir));
61
+ const getSignedViewerUrl = async (source) => {
62
+ return `https://${await host}:${await port}/${source.path}`;
63
+ };
15
64
  const getFileContent = async (source) => {
16
65
  const filePath = path_1.default.join(await fileDir, source.path);
17
66
  return fs_1.default.promises.readFile(filePath);
18
67
  };
68
+ const putFileContent = async (source, content) => {
69
+ const filePath = path_1.default.join(await fileDir, source.path);
70
+ const directory = path_1.default.dirname(filePath);
71
+ await fs_1.default.promises.mkdir(directory, { recursive: true });
72
+ await fs_1.default.promises.writeFile(filePath, content);
73
+ return source;
74
+ };
19
75
  return {
76
+ getSignedViewerUrl,
20
77
  getFileContent,
78
+ putFileContent,
21
79
  };
22
80
  };
23
81
  exports.localFileServer = localFileServer;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.s3FileServer = void 0;
4
+ /* cspell:ignore presigner */
4
5
  const client_s3_1 = require("@aws-sdk/client-s3");
6
+ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
5
7
  const __1 = require("../..");
6
8
  const assertions_1 = require("../../assertions");
7
9
  const config_1 = require("../../config");
@@ -12,14 +14,37 @@ const s3FileServer = (initializer) => (configProvider) => {
12
14
  const bucketRegion = (0, __1.once)(() => (0, config_1.resolveConfigValue)(config.bucketRegion));
13
15
  const client = (0, guards_1.ifDefined)(initializer.s3Client, client_s3_1.S3Client);
14
16
  const s3Service = (0, __1.once)(async () => new client({ apiVersion: '2012-08-10', region: await bucketRegion() }));
17
+ /*
18
+ * https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
19
+ */
20
+ const getSignedViewerUrl = async (source) => {
21
+ const bucket = (await bucketName());
22
+ const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: source.path });
23
+ return (0, s3_request_presigner_1.getSignedUrl)(await s3Service(), command, {
24
+ expiresIn: 3600, // 1 hour
25
+ });
26
+ };
15
27
  const getFileContent = async (source) => {
16
28
  const bucket = await bucketName();
17
29
  const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: source.path });
18
30
  const response = await (await s3Service()).send(command);
19
31
  return Buffer.from(await (0, assertions_1.assertDefined)(response.Body, new Error('Invalid Response from s3')).transformToByteArray());
20
32
  };
33
+ const putFileContent = async (source, content) => {
34
+ const bucket = await bucketName();
35
+ const command = new client_s3_1.PutObjectCommand({
36
+ Bucket: bucket,
37
+ Key: source.path,
38
+ Body: content,
39
+ ContentType: source.mimeType,
40
+ });
41
+ await (await s3Service()).send(command);
42
+ return source;
43
+ };
21
44
  return {
22
45
  getFileContent,
46
+ putFileContent,
47
+ getSignedViewerUrl,
23
48
  };
24
49
  };
25
50
  exports.s3FileServer = s3FileServer;
@@ -0,0 +1,29 @@
1
+ import { RequestBody } from '@opensearch-project/opensearch/lib/Transport';
2
+ import { ConfigProviderForConfig } from '../config';
3
+ import { IndexOptions, SearchOptions } from './searchProvider';
4
+ export declare type Config = {
5
+ node: string;
6
+ region: string;
7
+ };
8
+ export interface Initializer<C> {
9
+ configSpace?: C;
10
+ }
11
+ export declare type IndexConfig = {
12
+ name: string;
13
+ mappings: Record<string, any>;
14
+ pageSize?: number;
15
+ };
16
+ export declare const openSearchService: <T extends RequestBody<Record<string, any>>, C extends string = "deployed">(initializer?: Initializer<C>) => (indexConfig: IndexConfig, configProvider: { [key in C]: {
17
+ node: import("../config").ConfigValueProvider<string>;
18
+ region: import("../config").ConfigValueProvider<string>;
19
+ }; }) => {
20
+ ensureIndexCreated: () => Promise<void>;
21
+ index: (params: IndexOptions<T>) => Promise<void>;
22
+ search: (options: SearchOptions) => Promise<{
23
+ items: Exclude<T, undefined>[];
24
+ pageSize: number;
25
+ currentPage: number;
26
+ totalItems: number;
27
+ totalPages: number;
28
+ }>;
29
+ };
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openSearchService = void 0;
4
+ // cspell:ignore opensearch, Sigv
5
+ const credential_provider_node_1 = require("@aws-sdk/credential-provider-node");
6
+ const opensearch_1 = require("@opensearch-project/opensearch");
7
+ const aws_1 = require("@opensearch-project/opensearch/aws");
8
+ const config_1 = require("../config");
9
+ const guards_1 = require("../guards");
10
+ const helpers_1 = require("../misc/helpers");
11
+ const openSearchService = (initializer = {}) => (indexConfig, configProvider) => {
12
+ const config = configProvider[(0, guards_1.ifDefined)(initializer.configSpace, 'deployed')];
13
+ const pageSize = indexConfig.pageSize || 10;
14
+ const client = (0, helpers_1.once)(async () => new opensearch_1.Client({
15
+ ...(0, aws_1.AwsSigv4Signer)({
16
+ getCredentials: () => (0, credential_provider_node_1.defaultProvider)()(),
17
+ region: await (0, config_1.resolveConfigValue)(config.region),
18
+ service: 'es',
19
+ }),
20
+ node: await (0, config_1.resolveConfigValue)(config.node),
21
+ }));
22
+ const createIndexIfNotExists = async (indices, params) => {
23
+ const { index } = params;
24
+ const { body } = await indices.exists({ index });
25
+ if (!body) {
26
+ await indices.create(params);
27
+ }
28
+ };
29
+ const ensureIndexCreated = async () => {
30
+ const { indices } = await client();
31
+ await createIndexIfNotExists(indices, {
32
+ index: indexConfig.name,
33
+ body: {
34
+ mappings: {
35
+ dynamic: false,
36
+ properties: indexConfig.mappings
37
+ }
38
+ }
39
+ });
40
+ };
41
+ const index = async (params) => {
42
+ const openSearchClient = await client();
43
+ await openSearchClient.index({
44
+ index: indexConfig.name,
45
+ body: params.body,
46
+ id: params.id,
47
+ refresh: true
48
+ });
49
+ };
50
+ const search = async (options) => {
51
+ const body = { query: { bool: {} } };
52
+ if (options.query) {
53
+ body.query.bool.must = {
54
+ multi_match: {
55
+ fields: options.fields.map((field) => 'weight' in field ? `${field.key}^${field.weight}` : field.key),
56
+ query: options.query
57
+ }
58
+ };
59
+ }
60
+ const { must_not } = options;
61
+ const must = 'filter' in options ? options.filter : options.must;
62
+ if (must && must.length > 0) {
63
+ body.query.bool.filter = [];
64
+ must.forEach((filter) => {
65
+ const { key } = filter;
66
+ const values = filter.value instanceof Array ? filter.value : [filter.value];
67
+ body.query.bool.filter.push({ terms: { [key]: values } });
68
+ });
69
+ }
70
+ if (must_not && must_not.length > 0) {
71
+ body.query.bool.must_not = [];
72
+ must_not.forEach((filter) => {
73
+ const { key } = filter;
74
+ const values = filter.value instanceof Array ? filter.value : [filter.value];
75
+ values.forEach((value) => body.query.bool.must_not.push({ term: { [key]: value } }));
76
+ });
77
+ }
78
+ if (options.should && options.should.length > 0) {
79
+ body.query.bool.should = options.should.map(term => {
80
+ const { key } = term;
81
+ const values = term.value instanceof Array ? term.value : [term.value];
82
+ return { terms: { [key]: values } };
83
+ });
84
+ body.query.bool.minimum_should_match = 1;
85
+ }
86
+ if (options.page) {
87
+ body.size = pageSize;
88
+ body.from = (options.page - 1) * pageSize;
89
+ }
90
+ const response = await (await client()).search({
91
+ body,
92
+ index: indexConfig.name
93
+ });
94
+ if (response.statusCode !== 200) {
95
+ throw new Error(`Unexpected status code: ${response.statusCode} from OpenSearch`);
96
+ }
97
+ const hits = response.body.hits;
98
+ const items = hits.hits.map((hit) => hit._source).filter(guards_1.isDefined);
99
+ const currentPage = options.page || 1;
100
+ const { total } = hits;
101
+ const totalItems = typeof total === 'number' ? total : total.value;
102
+ const totalPages = Math.ceil(totalItems / pageSize) || 1;
103
+ return { items, pageSize, currentPage, totalItems, totalPages };
104
+ };
105
+ return { ensureIndexCreated, index, search };
106
+ };
107
+ exports.openSearchService = openSearchService;
@@ -18,7 +18,9 @@ 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: postgres_1.default.camel,
21
+ transform: {
22
+ column: { to: postgres_1.default.fromCamel, from: postgres_1.default.toCamel },
23
+ },
22
24
  }));
23
25
  const connections = [];
24
26
  const sql = (0, helpers_1.once)(async () => {
@@ -27,5 +27,6 @@ export interface SearchOptions {
27
27
  filter?: Filter[];
28
28
  must?: Filter[];
29
29
  must_not?: Filter[];
30
+ should?: Filter[];
30
31
  }
31
32
  export {};
@@ -10,6 +10,7 @@ var MatchType;
10
10
  (function (MatchType) {
11
11
  MatchType[MatchType["Must"] = 0] = "Must";
12
12
  MatchType[MatchType["MustNot"] = 1] = "MustNot";
13
+ MatchType[MatchType["Should"] = 2] = "Should";
13
14
  })(MatchType || (MatchType = {}));
14
15
  const resolveField = (document, field) => field.key.toString().split('.').reduce((result, key) => result[key], document);
15
16
  const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
@@ -21,7 +22,7 @@ const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
21
22
  const results = (await loadAllDocumentsTheBadWay())
22
23
  .map(document => {
23
24
  let weight = 0;
24
- const matchFilters = (filters, mustMatch) => {
25
+ const matchFilters = (filters, matchType) => {
25
26
  for (const field of filters) {
26
27
  const docValues = (0, __1.coerceArray)(resolveField(document, field));
27
28
  const coerceValue = getFieldType(field) === 'boolean'
@@ -36,11 +37,14 @@ const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
36
37
  }
37
38
  : (x) => x;
38
39
  const hasMatch = (0, __1.coerceArray)(field.value).map(coerceValue).some(v => docValues.includes(v));
39
- if ((mustMatch === MatchType.Must && !hasMatch) || (mustMatch === MatchType.MustNot && hasMatch)) {
40
+ if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
40
41
  return false;
41
42
  }
43
+ else if (matchType === MatchType.Should && hasMatch) {
44
+ return true;
45
+ }
42
46
  }
43
- return true;
47
+ return matchType !== MatchType.Should;
44
48
  };
45
49
  if (options.query !== undefined) {
46
50
  for (const field of options.fields) {
@@ -61,7 +65,7 @@ const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
61
65
  }
62
66
  }
63
67
  }
64
- const { must_not } = options;
68
+ const { must_not, should } = options;
65
69
  const must = 'filter' in options ? options.filter : options.must;
66
70
  if ((must === null || must === void 0 ? void 0 : must.length) && !matchFilters(must, MatchType.Must)) {
67
71
  return undefined;
@@ -69,6 +73,9 @@ const memorySearchTheBadWay = ({ loadAllDocumentsTheBadWay }) => {
69
73
  if ((must_not === null || must_not === void 0 ? void 0 : must_not.length) && !matchFilters(must_not, MatchType.MustNot)) {
70
74
  return undefined;
71
75
  }
76
+ if ((should === null || should === void 0 ? void 0 : should.length) && !matchFilters(should, MatchType.Should)) {
77
+ return undefined;
78
+ }
72
79
  return { document, weight };
73
80
  })
74
81
  .filter(guards_1.isDefined)