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