@openstax/ts-utils 1.26.1 → 1.27.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.
- package/dist/cjs/middleware/lambdaCorsResponseMiddleware.js +1 -1
- package/dist/cjs/services/authProvider/decryption.js +3 -1
- package/dist/cjs/services/authProvider/index.d.ts +2 -1
- package/dist/cjs/services/authProvider/index.js +4 -1
- package/dist/cjs/services/authProvider/subrequest.js +2 -0
- package/dist/cjs/services/documentStore/unversioned/dynamodb.d.ts +4 -0
- package/dist/cjs/services/documentStore/unversioned/dynamodb.js +31 -0
- package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +4 -0
- package/dist/cjs/services/documentStore/unversioned/file-system.js +19 -7
- package/dist/cjs/services/searchProvider/index.d.ts +26 -7
- package/dist/cjs/services/searchProvider/memorySearchTheBadWay.d.ts +6 -3
- package/dist/cjs/services/searchProvider/memorySearchTheBadWay.js +98 -41
- package/dist/cjs/services/searchProvider/openSearch.d.ts +3 -2
- package/dist/cjs/services/searchProvider/openSearch.js +25 -10
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/middleware/lambdaCorsResponseMiddleware.js +1 -1
- package/dist/esm/services/authProvider/decryption.js +3 -1
- package/dist/esm/services/authProvider/index.d.ts +2 -1
- package/dist/esm/services/authProvider/index.js +4 -1
- package/dist/esm/services/authProvider/subrequest.js +2 -0
- package/dist/esm/services/documentStore/unversioned/dynamodb.d.ts +4 -0
- package/dist/esm/services/documentStore/unversioned/dynamodb.js +32 -1
- package/dist/esm/services/documentStore/unversioned/file-system.d.ts +4 -0
- package/dist/esm/services/documentStore/unversioned/file-system.js +19 -7
- package/dist/esm/services/searchProvider/index.d.ts +26 -7
- package/dist/esm/services/searchProvider/memorySearchTheBadWay.d.ts +6 -3
- package/dist/esm/services/searchProvider/memorySearchTheBadWay.js +98 -41
- package/dist/esm/services/searchProvider/openSearch.d.ts +3 -2
- package/dist/esm/services/searchProvider/openSearch.js +25 -10
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ const createLambdaCorsResponseMiddleware = (config) => () => (responsePromise, {
|
|
|
21
21
|
'Access-Control-Allow-Origin': request.headers.origin,
|
|
22
22
|
'Access-Control-Allow-Credentials': 'true',
|
|
23
23
|
'Access-Control-Allow-Headers': 'content-type, x-request-id',
|
|
24
|
-
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
|
24
|
+
'Access-Control-Allow-Methods': 'POST, GET, PUT, PATCH, DELETE, OPTIONS',
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
};
|
|
@@ -14,6 +14,7 @@ const decryptionAuthProvider = (initializer) => (configProvider) => {
|
|
|
14
14
|
const signaturePublicKey = (0, helpers_1.once)(() => (0, resolveConfigValue_1.resolveConfigValue)(config.signaturePublicKey));
|
|
15
15
|
return ({ request, logger }) => {
|
|
16
16
|
let user;
|
|
17
|
+
const getAuthToken = async () => (0, _1.getAuthTokenOrCookie)(request, await cookieName())[0];
|
|
17
18
|
const getAuthorizedFetchConfig = async () => {
|
|
18
19
|
const [token, headers] = (0, _1.getAuthTokenOrCookie)(request, await cookieName());
|
|
19
20
|
if (!token) {
|
|
@@ -22,7 +23,7 @@ const decryptionAuthProvider = (initializer) => (configProvider) => {
|
|
|
22
23
|
return { headers };
|
|
23
24
|
};
|
|
24
25
|
const getPayload = async (tokenString) => {
|
|
25
|
-
const token = tokenString !== null && tokenString !== void 0 ? tokenString :
|
|
26
|
+
const token = tokenString !== null && tokenString !== void 0 ? tokenString : await getAuthToken();
|
|
26
27
|
if (!token) {
|
|
27
28
|
return undefined;
|
|
28
29
|
}
|
|
@@ -43,6 +44,7 @@ const decryptionAuthProvider = (initializer) => (configProvider) => {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
};
|
|
45
46
|
return {
|
|
47
|
+
getAuthToken,
|
|
46
48
|
getAuthorizedFetchConfig,
|
|
47
49
|
getTokenExpiration: async (tokenString) => {
|
|
48
50
|
var _a;
|
|
@@ -37,6 +37,7 @@ export interface ApiUser extends TokenUser {
|
|
|
37
37
|
}
|
|
38
38
|
export declare type User = TokenUser | ApiUser;
|
|
39
39
|
export declare type AuthProvider = {
|
|
40
|
+
getAuthToken: () => Promise<string | null>;
|
|
40
41
|
getUser: () => Promise<User | undefined>;
|
|
41
42
|
/**
|
|
42
43
|
* gets second argument for `fetch` that has authentication token or cookie
|
|
@@ -58,4 +59,4 @@ export declare const getAuthTokenOrCookie: (request: CookieAuthProviderRequest,
|
|
|
58
59
|
Authorization: string;
|
|
59
60
|
}] | [string, {
|
|
60
61
|
cookie: string;
|
|
61
|
-
}];
|
|
62
|
+
}] | [null, {}];
|
|
@@ -8,6 +8,7 @@ const cookie_1 = __importDefault(require("cookie"));
|
|
|
8
8
|
const helpers_1 = require("../../misc/helpers");
|
|
9
9
|
const helpers_2 = require("../../routing/helpers");
|
|
10
10
|
const stubAuthProvider = (user) => ({
|
|
11
|
+
getAuthToken: () => Promise.resolve('authToken'),
|
|
11
12
|
getUser: () => Promise.resolve(user),
|
|
12
13
|
getAuthorizedFetchConfig: () => Promise.resolve(user ? { headers: { Authorization: user.uuid } } : {})
|
|
13
14
|
});
|
|
@@ -21,6 +22,8 @@ const getAuthTokenOrCookie = (request, cookieName, queryKey = 'auth') => {
|
|
|
21
22
|
? (0, helpers_1.tuple)(authParam, { Authorization: `Bearer ${authParam}` })
|
|
22
23
|
: authHeader && authHeader.length >= 8 && authHeader.startsWith('Bearer ')
|
|
23
24
|
? (0, helpers_1.tuple)(authHeader.slice(7), { Authorization: authHeader })
|
|
24
|
-
:
|
|
25
|
+
: cookieValue
|
|
26
|
+
? (0, helpers_1.tuple)(cookieValue, { cookie: cookie_1.default.serialize(cookieName, cookieValue) })
|
|
27
|
+
: (0, helpers_1.tuple)(null, {});
|
|
25
28
|
};
|
|
26
29
|
exports.getAuthTokenOrCookie = getAuthTokenOrCookie;
|
|
@@ -15,6 +15,7 @@ const subrequestAuthProvider = (initializer) => (configProvider) => {
|
|
|
15
15
|
const accountsBase = (0, __1.once)(() => (0, config_1.resolveConfigValue)(config.accountsBase));
|
|
16
16
|
return ({ request, logger }) => {
|
|
17
17
|
let user;
|
|
18
|
+
const getAuthToken = async () => (0, _1.getAuthTokenOrCookie)(request, await cookieName())[0];
|
|
18
19
|
const getAuthorizedFetchConfig = async () => {
|
|
19
20
|
const [token, headers] = (0, _1.getAuthTokenOrCookie)(request, await cookieName());
|
|
20
21
|
if (!token) {
|
|
@@ -37,6 +38,7 @@ const subrequestAuthProvider = (initializer) => (configProvider) => {
|
|
|
37
38
|
return user;
|
|
38
39
|
};
|
|
39
40
|
return {
|
|
41
|
+
getAuthToken,
|
|
40
42
|
getAuthorizedFetchConfig,
|
|
41
43
|
getUser: async () => {
|
|
42
44
|
if (!user) {
|
|
@@ -9,6 +9,10 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
|
|
|
9
9
|
afterWrite?: ((item: T) => void | Promise<void>) | undefined;
|
|
10
10
|
} | undefined) => {
|
|
11
11
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
12
|
+
getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
|
|
13
|
+
items: T[];
|
|
14
|
+
nextPageToken: string | undefined;
|
|
15
|
+
}>;
|
|
12
16
|
batchGetItem: (ids: T[K][]) => Promise<T[]>;
|
|
13
17
|
getItem: (id: T[K]) => Promise<T | undefined>;
|
|
14
18
|
incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
|
|
@@ -25,6 +25,37 @@ const dynamoUnversionedDocumentStore = (initializer) => () => (configProvider) =
|
|
|
25
25
|
};
|
|
26
26
|
return loadAllResults();
|
|
27
27
|
},
|
|
28
|
+
/*
|
|
29
|
+
* requires that a global secondary index exist on the table
|
|
30
|
+
*/
|
|
31
|
+
getItemsByField: async (key, value, pageKey) => {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
const ExclusiveStartKey = pageKey
|
|
34
|
+
? JSON.parse(Buffer.from(pageKey, 'base64').toString('utf-8'))
|
|
35
|
+
: undefined;
|
|
36
|
+
const table = await tableName();
|
|
37
|
+
const cmd = new client_dynamodb_1.QueryCommand({
|
|
38
|
+
TableName: table,
|
|
39
|
+
IndexName: key.toString(),
|
|
40
|
+
ExclusiveStartKey,
|
|
41
|
+
KeyConditionExpression: '#hk = :hkv',
|
|
42
|
+
ExpressionAttributeValues: {
|
|
43
|
+
':hkv': (0, dynamoEncoding_1.encodeDynamoAttribute)(value),
|
|
44
|
+
},
|
|
45
|
+
ExpressionAttributeNames: {
|
|
46
|
+
'#hk': key.toString(),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const response = await dynamodb().send(cmd);
|
|
50
|
+
const items = (_b = (_a = response.Items) === null || _a === void 0 ? void 0 : _a.map((item) => (0, dynamoEncoding_1.decodeDynamoDocument)(item))) !== null && _b !== void 0 ? _b : [];
|
|
51
|
+
const nextPageToken = response.LastEvaluatedKey;
|
|
52
|
+
return {
|
|
53
|
+
items,
|
|
54
|
+
nextPageToken: nextPageToken
|
|
55
|
+
? Buffer.from(JSON.stringify(nextPageToken)).toString('base64')
|
|
56
|
+
: undefined,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
28
59
|
batchGetItem: async (ids) => {
|
|
29
60
|
const table = await tableName();
|
|
30
61
|
const key = hashKey.toString();
|
|
@@ -9,6 +9,10 @@ export declare const fileSystemUnversionedDocumentStore: <C extends string = "fi
|
|
|
9
9
|
tableName: import("../../../config").ConfigValueProvider<string>;
|
|
10
10
|
}; }) => <K extends keyof T>(_: {}, hashKey: K) => {
|
|
11
11
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
12
|
+
getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
|
|
13
|
+
items: T[];
|
|
14
|
+
nextPageToken: string | undefined;
|
|
15
|
+
}>;
|
|
12
16
|
batchGetItem: (ids: T[K][]) => Promise<Exclude<Awaited<T>, undefined>[]>;
|
|
13
17
|
getItem: (id: T[K]) => Promise<T | undefined>;
|
|
14
18
|
incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
|
|
@@ -60,14 +60,26 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
|
+
const loadAllDocumentsTheBadWay = async () => {
|
|
64
|
+
const path = await tablePath;
|
|
65
|
+
await mkTableDir;
|
|
66
|
+
return new Promise((resolve, reject) => readdir(path, (err, files) => err ?
|
|
67
|
+
reject(err) :
|
|
68
|
+
Promise.all(files.map((file) => load(file)))
|
|
69
|
+
.then((allData) => resolve(allData.filter(guards_1.isDefined)), (err) => reject(err))));
|
|
70
|
+
};
|
|
63
71
|
return {
|
|
64
|
-
loadAllDocumentsTheBadWay
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
loadAllDocumentsTheBadWay,
|
|
73
|
+
getItemsByField: async (key, value, pageKey) => {
|
|
74
|
+
const pageSize = 10;
|
|
75
|
+
const items = await loadAllDocumentsTheBadWay();
|
|
76
|
+
const filteredItems = items.filter((item) => item[key] === value);
|
|
77
|
+
const startIndex = pageKey ? parseInt(pageKey, 10) : 0;
|
|
78
|
+
const paginatedItems = filteredItems.slice(startIndex, startIndex + pageSize);
|
|
79
|
+
const nextPageToken = startIndex + pageSize < filteredItems.length
|
|
80
|
+
? (startIndex + pageSize).toString()
|
|
81
|
+
: undefined;
|
|
82
|
+
return { items: paginatedItems, nextPageToken };
|
|
71
83
|
},
|
|
72
84
|
batchGetItem: async (ids) => {
|
|
73
85
|
const items = await Promise.all(ids.map((id) => load(hashFilename(id))));
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
export declare type FieldType = string | string[] | number | boolean;
|
|
2
|
-
export declare type
|
|
3
|
-
key: string;
|
|
4
|
-
value: FieldType;
|
|
5
|
-
} | {
|
|
2
|
+
export declare type ESFilter = {
|
|
6
3
|
terms: Record<string, FieldType>;
|
|
7
4
|
} | {
|
|
8
5
|
exists: {
|
|
9
6
|
field: string;
|
|
10
7
|
};
|
|
8
|
+
} | {
|
|
9
|
+
nested: {
|
|
10
|
+
path: string;
|
|
11
|
+
query: Filter;
|
|
12
|
+
};
|
|
13
|
+
} | {
|
|
14
|
+
bool: BoolFilter;
|
|
15
|
+
};
|
|
16
|
+
export declare type Filter = {
|
|
17
|
+
key: string;
|
|
18
|
+
value: FieldType;
|
|
19
|
+
} | ESFilter;
|
|
20
|
+
export declare type BoolFilter = {
|
|
21
|
+
must?: Filter[];
|
|
22
|
+
must_not?: Filter[];
|
|
23
|
+
should?: Filter[];
|
|
24
|
+
filter?: Filter[];
|
|
25
|
+
minimum_should_match?: number;
|
|
11
26
|
};
|
|
12
27
|
declare type Field = {
|
|
13
28
|
key: string;
|
|
@@ -24,13 +39,17 @@ export interface IndexOptions<T> {
|
|
|
24
39
|
body: T;
|
|
25
40
|
id: string;
|
|
26
41
|
}
|
|
42
|
+
export declare type FieldMapping = {
|
|
43
|
+
type: 'keyword' | 'text' | 'boolean';
|
|
44
|
+
} | {
|
|
45
|
+
type?: 'nested' | 'object';
|
|
46
|
+
properties: Record<string, FieldMapping>;
|
|
47
|
+
};
|
|
48
|
+
export declare type FieldMappings = Record<string, FieldMapping>;
|
|
27
49
|
export interface SearchOptions {
|
|
28
50
|
page?: number;
|
|
29
51
|
query: string | undefined;
|
|
30
52
|
fields: Field[];
|
|
31
|
-
/**
|
|
32
|
-
* @deprecated use `must` instead
|
|
33
|
-
*/
|
|
34
53
|
filter?: Filter[];
|
|
35
54
|
must?: Filter[];
|
|
36
55
|
must_not?: Filter[];
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { IndexOptions, SearchOptions } from '.';
|
|
2
|
-
export declare
|
|
1
|
+
import { FieldMappings, IndexOptions, SearchOptions } from '.';
|
|
2
|
+
export declare type Config<T> = {
|
|
3
|
+
mappings: FieldMappings;
|
|
3
4
|
store: {
|
|
4
5
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
5
6
|
};
|
|
6
|
-
}
|
|
7
|
+
};
|
|
8
|
+
export declare const memorySearchTheBadWay: () => <T>({ store, mappings }: Config<T>) => {
|
|
7
9
|
deleteIndexIfExists: () => Promise<undefined>;
|
|
8
10
|
ensureIndexCreated: () => Promise<undefined>;
|
|
9
11
|
index: (_options: IndexOptions<T>) => Promise<undefined>;
|
|
12
|
+
bulkIndex: (_items: IndexOptions<T>[]) => Promise<undefined>;
|
|
10
13
|
search: (options: SearchOptions) => Promise<{
|
|
11
14
|
items: T[];
|
|
12
15
|
pageSize: number;
|
|
@@ -12,16 +12,58 @@ var MatchType;
|
|
|
12
12
|
MatchType[MatchType["MustNot"] = 1] = "MustNot";
|
|
13
13
|
MatchType[MatchType["Should"] = 2] = "Should";
|
|
14
14
|
})(MatchType || (MatchType = {}));
|
|
15
|
-
const resolveField = (
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const resolveField = (context, field) => {
|
|
16
|
+
const path = field.startsWith(context.scope + '.')
|
|
17
|
+
? field.slice(context.scope.length + 1)
|
|
18
|
+
: field;
|
|
19
|
+
return path.split('.').reduce((result, key) => {
|
|
20
|
+
if (result === undefined) {
|
|
21
|
+
throw new Error(`Key "${key}" not found in document resolving ${field}`);
|
|
22
|
+
}
|
|
23
|
+
return result[key];
|
|
24
|
+
}, context.document);
|
|
25
|
+
};
|
|
26
|
+
const getFieldDefinition = (context, field) => {
|
|
27
|
+
let current = context.fields;
|
|
28
|
+
const parts = field.split('.');
|
|
29
|
+
const head = parts.slice(0, -1);
|
|
30
|
+
const tail = parts[parts.length - 1];
|
|
31
|
+
for (const part of head) {
|
|
32
|
+
const currentDefinition = current[part];
|
|
33
|
+
if (!currentDefinition || !('properties' in currentDefinition)) {
|
|
34
|
+
throw new Error(`Field "${field}" not found in mappings`);
|
|
35
|
+
}
|
|
36
|
+
current = currentDefinition.properties;
|
|
37
|
+
}
|
|
38
|
+
if (!current[tail]) {
|
|
39
|
+
throw new Error(`Field "${field}" not found in mappings`);
|
|
40
|
+
}
|
|
41
|
+
return current[tail];
|
|
42
|
+
};
|
|
43
|
+
const getFieldType = (context, field) => {
|
|
44
|
+
const definition = getFieldDefinition(context, field);
|
|
45
|
+
return definition.type;
|
|
46
|
+
};
|
|
47
|
+
const matchNested = (context, nested) => {
|
|
48
|
+
const nestedPath = nested.path;
|
|
49
|
+
const nestedDocuments = resolveField(context, nestedPath);
|
|
50
|
+
if (!Array.isArray(nestedDocuments)) {
|
|
51
|
+
throw new errors_1.InvalidRequestError(`Nested path "${nestedPath}" is not an array`);
|
|
52
|
+
}
|
|
53
|
+
return nestedDocuments.some(nestedDocument => matchClause({
|
|
54
|
+
...context,
|
|
55
|
+
scope: nestedPath,
|
|
56
|
+
document: nestedDocument
|
|
57
|
+
}, nested.query));
|
|
58
|
+
};
|
|
59
|
+
const matchExists = (context, exists) => {
|
|
60
|
+
const value = resolveField(context, exists.field);
|
|
18
61
|
return value !== undefined && value !== null;
|
|
19
62
|
};
|
|
20
|
-
const matchTerms = (
|
|
21
|
-
const getFieldType = (field) => { var _a; return (_a = options.fields.find(f => f.key == field)) === null || _a === void 0 ? void 0 : _a.type; };
|
|
63
|
+
const matchTerms = (context, terms) => {
|
|
22
64
|
for (const key in terms) {
|
|
23
|
-
const docValues = (0, __1.coerceArray)(resolveField(
|
|
24
|
-
const coerceValue = getFieldType(key) === 'boolean'
|
|
65
|
+
const docValues = (0, __1.coerceArray)(resolveField(context, key));
|
|
66
|
+
const coerceValue = getFieldType(context, key) === 'boolean'
|
|
25
67
|
? (input) => {
|
|
26
68
|
if ([true, 'true', '1', 1].includes(input)) {
|
|
27
69
|
return true;
|
|
@@ -39,43 +81,66 @@ const matchTerms = (options, terms, document) => {
|
|
|
39
81
|
}
|
|
40
82
|
return true;
|
|
41
83
|
};
|
|
42
|
-
const
|
|
84
|
+
const matchClause = (context, clause) => {
|
|
85
|
+
const filter = ('key' in clause && 'value' in clause)
|
|
86
|
+
? { terms: { [clause.key]: clause.value } }
|
|
87
|
+
: clause;
|
|
88
|
+
if ('terms' in filter)
|
|
89
|
+
return matchTerms(context, filter.terms);
|
|
90
|
+
else if ('exists' in filter)
|
|
91
|
+
return matchExists(context, filter.exists);
|
|
92
|
+
else if ('bool' in filter)
|
|
93
|
+
return matchBool(context, filter.bool);
|
|
94
|
+
else if ('nested' in filter)
|
|
95
|
+
return matchNested(context, filter.nested);
|
|
96
|
+
else
|
|
97
|
+
throw new errors_1.InvalidRequestError('invalid filter type');
|
|
98
|
+
};
|
|
99
|
+
const matchFilters = (context, filters, matchType) => {
|
|
100
|
+
for (const field of filters) {
|
|
101
|
+
const hasMatch = matchClause(context, field);
|
|
102
|
+
if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
else if (matchType === MatchType.Should && hasMatch) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return matchType !== MatchType.Should;
|
|
110
|
+
};
|
|
111
|
+
const matchBool = (context, filters) => {
|
|
112
|
+
const { must_not, should, must, filter } = filters;
|
|
113
|
+
if ((must === null || must === void 0 ? void 0 : must.length) && !matchFilters(context, must, MatchType.Must)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if ((filter === null || filter === void 0 ? void 0 : filter.length) && !matchFilters(context, filter, MatchType.Must)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if ((must_not === null || must_not === void 0 ? void 0 : must_not.length) && !matchFilters(context, must_not, MatchType.MustNot)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if ((should === null || should === void 0 ? void 0 : should.length) && !matchFilters(context, should, MatchType.Should)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
const memorySearchTheBadWay = () => ({ store, mappings }) => {
|
|
43
128
|
return {
|
|
44
129
|
// This method is intentionally stubbed because index deletion is not applicable for in-memory storage.
|
|
45
130
|
deleteIndexIfExists: async () => undefined,
|
|
46
131
|
ensureIndexCreated: async () => undefined,
|
|
47
132
|
index: async (_options) => undefined,
|
|
133
|
+
bulkIndex: async (_items) => undefined,
|
|
48
134
|
search: async (options) => {
|
|
49
135
|
const results = (await store.loadAllDocumentsTheBadWay())
|
|
50
136
|
.map(document => {
|
|
51
137
|
let weight = 0;
|
|
52
|
-
const matchFilters = (filters, matchType) => {
|
|
53
|
-
for (const field of filters) {
|
|
54
|
-
const filter = ('key' in field && 'value' in field)
|
|
55
|
-
? { terms: { [field.key]: field.value } }
|
|
56
|
-
: field;
|
|
57
|
-
let hasMatch;
|
|
58
|
-
if ('terms' in filter)
|
|
59
|
-
hasMatch = matchTerms(options, filter.terms, document);
|
|
60
|
-
else if ('exists' in filter)
|
|
61
|
-
hasMatch = matchExists(filter.exists, document);
|
|
62
|
-
else
|
|
63
|
-
throw new errors_1.InvalidRequestError('invalid filter type');
|
|
64
|
-
if ((matchType === MatchType.Must && !hasMatch) || (matchType === MatchType.MustNot && hasMatch)) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
else if (matchType === MatchType.Should && hasMatch) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return matchType !== MatchType.Should;
|
|
72
|
-
};
|
|
73
138
|
if (options.query !== undefined) {
|
|
74
139
|
for (const field of options.fields) {
|
|
75
140
|
if (field.type !== undefined && field.type !== 'text') {
|
|
76
141
|
continue;
|
|
77
142
|
}
|
|
78
|
-
const value = resolveField(document, field.key);
|
|
143
|
+
const value = resolveField({ document, fields: mappings, scope: '' }, field.key);
|
|
79
144
|
if (value === undefined || value === null) {
|
|
80
145
|
continue;
|
|
81
146
|
}
|
|
@@ -89,15 +154,7 @@ const memorySearchTheBadWay = () => ({ store }) => {
|
|
|
89
154
|
}
|
|
90
155
|
}
|
|
91
156
|
}
|
|
92
|
-
|
|
93
|
-
const must = 'filter' in options ? options.filter : options.must;
|
|
94
|
-
if ((must === null || must === void 0 ? void 0 : must.length) && !matchFilters(must, MatchType.Must)) {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
if ((must_not === null || must_not === void 0 ? void 0 : must_not.length) && !matchFilters(must_not, MatchType.MustNot)) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
if ((should === null || should === void 0 ? void 0 : should.length) && !matchFilters(should, MatchType.Should)) {
|
|
157
|
+
if (!matchBool({ document, fields: mappings, scope: '' }, options)) {
|
|
101
158
|
return undefined;
|
|
102
159
|
}
|
|
103
160
|
return { document, weight };
|
|
@@ -106,8 +163,8 @@ const memorySearchTheBadWay = () => ({ store }) => {
|
|
|
106
163
|
.filter(r => !options.query || r.weight >= MIN_MATCH);
|
|
107
164
|
results.sort((a, b) => {
|
|
108
165
|
for (const sort of (options.sort || [])) {
|
|
109
|
-
const aValue = resolveField(a.document, sort.key);
|
|
110
|
-
const bValue = resolveField(b.document, sort.key);
|
|
166
|
+
const aValue = resolveField({ document: a.document, fields: mappings, scope: '' }, sort.key);
|
|
167
|
+
const bValue = resolveField({ document: b.document, fields: mappings, scope: '' }, sort.key);
|
|
111
168
|
if (aValue < bValue) {
|
|
112
169
|
return sort.order === 'asc' ? -1 : 1;
|
|
113
170
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigProviderForConfig } from '../../config';
|
|
2
|
-
import { IndexOptions, SearchOptions } from '.';
|
|
2
|
+
import { FieldMappings, IndexOptions, SearchOptions } from '.';
|
|
3
3
|
export declare type Config = {
|
|
4
4
|
node: string;
|
|
5
5
|
region: string;
|
|
@@ -9,7 +9,7 @@ export interface Initializer<C> {
|
|
|
9
9
|
}
|
|
10
10
|
export declare type IndexConfig = {
|
|
11
11
|
name: string;
|
|
12
|
-
mappings:
|
|
12
|
+
mappings: FieldMappings;
|
|
13
13
|
pageSize?: number;
|
|
14
14
|
};
|
|
15
15
|
export declare const openSearchService: <C extends string = "deployed">(initializer?: Initializer<C>) => (configProvider: { [key in C]: {
|
|
@@ -19,6 +19,7 @@ export declare const openSearchService: <C extends string = "deployed">(initiali
|
|
|
19
19
|
ensureIndexCreated: () => Promise<void>;
|
|
20
20
|
deleteIndexIfExists: () => Promise<void>;
|
|
21
21
|
index: (params: IndexOptions<T>) => Promise<void>;
|
|
22
|
+
bulkIndex: (items: IndexOptions<T>[]) => Promise<void>;
|
|
22
23
|
search: (options: SearchOptions) => Promise<{
|
|
23
24
|
items: Exclude<T, undefined>[];
|
|
24
25
|
pageSize: number;
|
|
@@ -63,24 +63,39 @@ const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
63
63
|
refresh: true
|
|
64
64
|
});
|
|
65
65
|
};
|
|
66
|
+
const bulkIndex = async (items) => {
|
|
67
|
+
const openSearchClient = await client();
|
|
68
|
+
await openSearchClient.bulk({
|
|
69
|
+
index: indexConfig.name,
|
|
70
|
+
body: items.flatMap((item) => [
|
|
71
|
+
{ index: { _id: item.id } },
|
|
72
|
+
item.body
|
|
73
|
+
]),
|
|
74
|
+
refresh: true
|
|
75
|
+
});
|
|
76
|
+
};
|
|
66
77
|
const search = async (options) => {
|
|
78
|
+
var _a;
|
|
67
79
|
const body = {
|
|
68
80
|
query: { bool: {} },
|
|
69
81
|
track_total_hits: true,
|
|
70
82
|
size: pageSize
|
|
71
83
|
};
|
|
72
84
|
if (options.query) {
|
|
73
|
-
body.query.bool.must = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
body.query.bool.must = [{
|
|
86
|
+
multi_match: {
|
|
87
|
+
fields: options.fields.map((field) => 'weight' in field ? `${field.key}^${field.weight}` : field.key),
|
|
88
|
+
query: options.query
|
|
89
|
+
}
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
const { must_not, should, must, filter } = options;
|
|
93
|
+
if (filter && filter.length > 0) {
|
|
94
|
+
body.query.bool.filter = filter.map(mapFilter);
|
|
79
95
|
}
|
|
80
|
-
const { must_not, should } = options;
|
|
81
|
-
const must = 'filter' in options ? options.filter : options.must;
|
|
82
96
|
if (must && must.length > 0) {
|
|
83
|
-
body.query.bool.
|
|
97
|
+
(_a = body.query.bool).must || (_a.must = []);
|
|
98
|
+
body.query.bool.must = body.query.bool.must.concat(must.map(mapFilter));
|
|
84
99
|
}
|
|
85
100
|
if (must_not && must_not.length > 0) {
|
|
86
101
|
body.query.bool.must_not = must_not.map(mapFilter);
|
|
@@ -112,7 +127,7 @@ const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
112
127
|
const totalPages = Math.ceil(totalItems / pageSize) || 1;
|
|
113
128
|
return { items, pageSize, currentPage, totalItems, totalPages };
|
|
114
129
|
};
|
|
115
|
-
return { ensureIndexCreated, deleteIndexIfExists, index, search };
|
|
130
|
+
return { ensureIndexCreated, deleteIndexIfExists, index, bulkIndex, search };
|
|
116
131
|
};
|
|
117
132
|
};
|
|
118
133
|
exports.openSearchService = openSearchService;
|