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