@openstax/ts-utils 1.27.6 → 1.28.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/errors/index.d.ts +11 -0
- package/dist/cjs/errors/index.js +15 -1
- package/dist/cjs/fetch/fetchStatusRetry.d.ts +1 -0
- package/dist/cjs/fetch/fetchStatusRetry.js +18 -7
- package/dist/cjs/guards/index.d.ts +8 -0
- package/dist/cjs/guards/index.js +10 -1
- package/dist/cjs/services/documentStore/unversioned/dynamodb.d.ts +9 -0
- package/dist/cjs/services/documentStore/unversioned/dynamodb.js +67 -0
- package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +12 -1
- package/dist/cjs/services/documentStore/unversioned/file-system.js +49 -1
- package/dist/cjs/services/searchProvider/openSearch.js +3 -0
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/errors/index.d.ts +11 -0
- package/dist/esm/errors/index.js +13 -0
- package/dist/esm/fetch/fetchStatusRetry.d.ts +1 -0
- package/dist/esm/fetch/fetchStatusRetry.js +18 -7
- package/dist/esm/guards/index.d.ts +8 -0
- package/dist/esm/guards/index.js +8 -0
- package/dist/esm/services/documentStore/unversioned/dynamodb.d.ts +9 -0
- package/dist/esm/services/documentStore/unversioned/dynamodb.js +65 -1
- package/dist/esm/services/documentStore/unversioned/file-system.d.ts +12 -1
- package/dist/esm/services/documentStore/unversioned/file-system.js +50 -2
- package/dist/esm/services/searchProvider/openSearch.js +3 -0
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +3 -1
|
@@ -75,3 +75,14 @@ export declare class SessionExpiredError extends Error {
|
|
|
75
75
|
static matches: (e: any) => e is typeof SessionExpiredError;
|
|
76
76
|
constructor(message?: string);
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Conflict error
|
|
80
|
+
*
|
|
81
|
+
* `ConflictError.matches(error)` is a reliable way to check if an error is a
|
|
82
|
+
* `ConflictError`; `instanceof` checks may not work if code is split into multiple bundles
|
|
83
|
+
*/
|
|
84
|
+
export declare class ConflictError extends Error {
|
|
85
|
+
static readonly TYPE = "ConflictError";
|
|
86
|
+
static matches: (e: any) => e is typeof ConflictError;
|
|
87
|
+
constructor(message?: string);
|
|
88
|
+
}
|
package/dist/esm/errors/index.js
CHANGED
|
@@ -97,3 +97,16 @@ export class SessionExpiredError extends Error {
|
|
|
97
97
|
}
|
|
98
98
|
SessionExpiredError.TYPE = 'SessionExpiredError';
|
|
99
99
|
SessionExpiredError.matches = errorIsType(SessionExpiredError);
|
|
100
|
+
/**
|
|
101
|
+
* Conflict error
|
|
102
|
+
*
|
|
103
|
+
* `ConflictError.matches(error)` is a reliable way to check if an error is a
|
|
104
|
+
* `ConflictError`; `instanceof` checks may not work if code is split into multiple bundles
|
|
105
|
+
*/
|
|
106
|
+
export class ConflictError extends Error {
|
|
107
|
+
constructor(message) {
|
|
108
|
+
super(message || ConflictError.TYPE);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
ConflictError.TYPE = 'ConflictError';
|
|
112
|
+
ConflictError.matches = errorIsType(ConflictError);
|
|
@@ -2,6 +2,7 @@ import { RetryOptions } from '../misc/helpers';
|
|
|
2
2
|
import { GenericFetch } from '.';
|
|
3
3
|
interface Options extends RetryOptions {
|
|
4
4
|
status?: number[];
|
|
5
|
+
timeout?: number;
|
|
5
6
|
}
|
|
6
7
|
export declare const fetchStatusRetry: (base: GenericFetch, options: Options) => GenericFetch;
|
|
7
8
|
export {};
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { retryWithDelay } from '../misc/helpers';
|
|
2
2
|
export const fetchStatusRetry = (base, options) => {
|
|
3
|
-
return (...params) => retryWithDelay(() =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
return (...params) => retryWithDelay(() => {
|
|
4
|
+
const fetchPromise = base(...params).then(r => {
|
|
5
|
+
var _a;
|
|
6
|
+
if ((_a = options.status) === null || _a === void 0 ? void 0 : _a.includes(r.status)) {
|
|
7
|
+
return r.text().then(text => {
|
|
8
|
+
throw new Error(`fetch failed ${params[0]} -- ${r.status}: ${text}`);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return r;
|
|
12
|
+
});
|
|
13
|
+
if (options.timeout) {
|
|
14
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
reject(new Error(`fetch timeout after ${options.timeout}ms: ${params[0]}`));
|
|
17
|
+
}, options.timeout);
|
|
8
18
|
});
|
|
19
|
+
return Promise.race([fetchPromise, timeoutPromise]);
|
|
9
20
|
}
|
|
10
|
-
return
|
|
11
|
-
}
|
|
21
|
+
return fetchPromise;
|
|
22
|
+
}, options);
|
|
12
23
|
};
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* `const result = (array as Array<string | undefined>).filter(isDefined);`
|
|
7
7
|
*/
|
|
8
8
|
export declare const isDefined: <X>(x: X) => x is Exclude<X, undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* checks if a thing is not null. while often easy to do with a simple if statement,
|
|
11
|
+
* in certain situations the guard is required and its nice to have one pre-defined.
|
|
12
|
+
* E.g. in the following example, the result is `Array<string>`.
|
|
13
|
+
*
|
|
14
|
+
* `const result = (array as Array<string | null>).filter(isNotNull);`
|
|
15
|
+
*/
|
|
16
|
+
export declare const isNotNull: <X>(x: X) => x is Exclude<X, null>;
|
|
9
17
|
/**
|
|
10
18
|
* checks if a thing is a number. while often easy to do with a simple if statement, in certain
|
|
11
19
|
* situations the guard is required and its nice to have one pre-defined. E.g. in the following
|
package/dist/esm/guards/index.js
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* `const result = (array as Array<string | undefined>).filter(isDefined);`
|
|
7
7
|
*/
|
|
8
8
|
export const isDefined = (x) => x !== undefined;
|
|
9
|
+
/**
|
|
10
|
+
* checks if a thing is not null. while often easy to do with a simple if statement,
|
|
11
|
+
* in certain situations the guard is required and its nice to have one pre-defined.
|
|
12
|
+
* E.g. in the following example, the result is `Array<string>`.
|
|
13
|
+
*
|
|
14
|
+
* `const result = (array as Array<string | null>).filter(isNotNull);`
|
|
15
|
+
*/
|
|
16
|
+
export const isNotNull = (x) => x !== null;
|
|
9
17
|
/**
|
|
10
18
|
* checks if a thing is a number. while often easy to do with a simple if statement, in certain
|
|
11
19
|
* situations the guard is required and its nice to have one pre-defined. E.g. in the following
|
|
@@ -9,6 +9,7 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
|
|
|
9
9
|
tableName: import("../../../config").ConfigValueProvider<string>;
|
|
10
10
|
}; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
|
|
11
11
|
afterWrite?: ((item: T) => void | Promise<void>) | undefined;
|
|
12
|
+
batchAfterWrite?: ((items: T[]) => void | Promise<void>) | undefined;
|
|
12
13
|
} | undefined) => {
|
|
13
14
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
14
15
|
getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
|
|
@@ -20,5 +21,13 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
|
|
|
20
21
|
incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
|
|
21
22
|
patchItem: (item: Partial<T>) => Promise<T>;
|
|
22
23
|
putItem: (item: T) => Promise<T>;
|
|
24
|
+
createItem: (item: T) => Promise<T>;
|
|
25
|
+
batchCreateItem: (items: T[], concurrency?: number) => Promise<{
|
|
26
|
+
successful: T[];
|
|
27
|
+
failed: {
|
|
28
|
+
item: T;
|
|
29
|
+
error: Error;
|
|
30
|
+
}[];
|
|
31
|
+
}>;
|
|
23
32
|
};
|
|
24
33
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { BatchGetItemCommand, DynamoDB, GetItemCommand, PutItemCommand, QueryCommand, ScanCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import asyncPool from 'tiny-async-pool';
|
|
2
3
|
import { once } from '../../..';
|
|
3
4
|
import { resolveConfigValue } from '../../../config';
|
|
4
|
-
import { NotFoundError } from '../../../errors';
|
|
5
|
+
import { ConflictError, NotFoundError } from '../../../errors';
|
|
5
6
|
import { ifDefined } from '../../../guards';
|
|
6
7
|
import { decodeDynamoDocument, encodeDynamoAttribute, encodeDynamoDocument } from '../dynamoEncoding';
|
|
7
8
|
export const dynamoUnversionedDocumentStore = (initializer) => {
|
|
@@ -157,6 +158,69 @@ export const dynamoUnversionedDocumentStore = (initializer) => {
|
|
|
157
158
|
await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, updatedDoc));
|
|
158
159
|
return updatedDoc;
|
|
159
160
|
},
|
|
161
|
+
/* creates a new document, fails if the primary key already exists */
|
|
162
|
+
createItem: async (item) => {
|
|
163
|
+
const cmd = new PutItemCommand({
|
|
164
|
+
TableName: await tableName(),
|
|
165
|
+
Item: encodeDynamoDocument(item),
|
|
166
|
+
ConditionExpression: 'attribute_not_exists(#k)',
|
|
167
|
+
ExpressionAttributeNames: {
|
|
168
|
+
'#k': hashKey.toString(),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return await dynamodb().send(cmd).then(() => item).catch((error) => {
|
|
172
|
+
throw error.name === 'ConditionalCheckFailedException' ?
|
|
173
|
+
new ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`) : error;
|
|
174
|
+
}).then(async (createdDoc) => {
|
|
175
|
+
var _a;
|
|
176
|
+
await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdDoc));
|
|
177
|
+
return createdDoc;
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
/* creates multiple new documents, returns successful and failed items separately */
|
|
181
|
+
batchCreateItem: async (items, concurrency = 10) => {
|
|
182
|
+
if (items.length === 0)
|
|
183
|
+
return { successful: [], failed: [] };
|
|
184
|
+
const successful = [];
|
|
185
|
+
const failed = [];
|
|
186
|
+
// Process using async iterator (tiny-async-pool v2)
|
|
187
|
+
for await (const result of asyncPool(concurrency, items, async (item) => {
|
|
188
|
+
var _a;
|
|
189
|
+
try {
|
|
190
|
+
const cmd = new PutItemCommand({
|
|
191
|
+
TableName: await tableName(),
|
|
192
|
+
Item: encodeDynamoDocument(item),
|
|
193
|
+
ConditionExpression: 'attribute_not_exists(#k)',
|
|
194
|
+
ExpressionAttributeNames: {
|
|
195
|
+
'#k': hashKey.toString(),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
await dynamodb().send(cmd);
|
|
199
|
+
// Only call individual afterWrite if batchAfterWrite is not defined
|
|
200
|
+
if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
|
|
201
|
+
await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
|
|
202
|
+
}
|
|
203
|
+
return { success: true, item };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const processedError = error.name === 'ConditionalCheckFailedException' ?
|
|
207
|
+
new ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`) : error;
|
|
208
|
+
return { success: false, item, error: processedError };
|
|
209
|
+
}
|
|
210
|
+
})) {
|
|
211
|
+
if (result.success) {
|
|
212
|
+
successful.push(result.item);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
failed.push({ item: result.item, error: result.error });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Call batchAfterWrite with all successful items if defined
|
|
219
|
+
if ((options === null || options === void 0 ? void 0 : options.batchAfterWrite) && successful.length > 0) {
|
|
220
|
+
await options.batchAfterWrite(successful);
|
|
221
|
+
}
|
|
222
|
+
return { successful, failed };
|
|
223
|
+
},
|
|
160
224
|
};
|
|
161
225
|
};
|
|
162
226
|
};
|
|
@@ -7,7 +7,10 @@ interface Initializer<C> {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const fileSystemUnversionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends TDocument<T>>() => (configProvider: { [key in C]: {
|
|
9
9
|
tableName: import("../../../config").ConfigValueProvider<string>;
|
|
10
|
-
}; }) => <K extends keyof T>(_: {}, hashKey: K
|
|
10
|
+
}; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
|
|
11
|
+
afterWrite?: ((item: T) => void | Promise<void>) | undefined;
|
|
12
|
+
batchAfterWrite?: ((items: T[]) => void | Promise<void>) | undefined;
|
|
13
|
+
} | undefined) => {
|
|
11
14
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
12
15
|
getItemsByField: (key: keyof T, value: T[K], pageKey?: string | undefined) => Promise<{
|
|
13
16
|
items: T[];
|
|
@@ -18,5 +21,13 @@ export declare const fileSystemUnversionedDocumentStore: <C extends string = "fi
|
|
|
18
21
|
incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
|
|
19
22
|
patchItem: (item: Partial<T>) => Promise<T>;
|
|
20
23
|
putItem: (item: T) => Promise<T>;
|
|
24
|
+
createItem: (item: T) => Promise<T>;
|
|
25
|
+
batchCreateItem: (items: T[], _concurrency?: number) => Promise<{
|
|
26
|
+
successful: T[];
|
|
27
|
+
failed: {
|
|
28
|
+
item: T;
|
|
29
|
+
error: Error;
|
|
30
|
+
}[];
|
|
31
|
+
}>;
|
|
21
32
|
};
|
|
22
33
|
export {};
|
|
@@ -2,9 +2,9 @@ import * as fsModule from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { hashValue } from '../../..';
|
|
4
4
|
import { resolveConfigValue } from '../../../config';
|
|
5
|
-
import { NotFoundError } from '../../../errors';
|
|
5
|
+
import { ConflictError, NotFoundError } from '../../../errors';
|
|
6
6
|
import { ifDefined, isDefined } from '../../../guards';
|
|
7
|
-
export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey) => {
|
|
7
|
+
export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
8
8
|
const tableName = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].tableName);
|
|
9
9
|
const tablePath = tableName.then((table) => path.join(initializer.dataDir, table));
|
|
10
10
|
const { mkdir, readdir, readFile, writeFile } = ifDefined(initializer.fs, fsModule);
|
|
@@ -99,5 +99,53 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
99
99
|
writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
|
|
100
100
|
});
|
|
101
101
|
},
|
|
102
|
+
createItem: async (item) => {
|
|
103
|
+
const hashed = hashFilename(item[hashKey]);
|
|
104
|
+
const existingItem = await load(hashed);
|
|
105
|
+
if (existingItem) {
|
|
106
|
+
throw new ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`);
|
|
107
|
+
}
|
|
108
|
+
const path = await filePath(hashed);
|
|
109
|
+
await mkTableDir;
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
batchCreateItem: async (items, _concurrency = 1) => {
|
|
115
|
+
var _a;
|
|
116
|
+
if (items.length === 0)
|
|
117
|
+
return { successful: [], failed: [] };
|
|
118
|
+
const successful = [];
|
|
119
|
+
const failed = [];
|
|
120
|
+
// Process items sequentially to ensure consistent conflict detection
|
|
121
|
+
// Note: concurrency parameter is ignored for filesystem to avoid race conditions
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
try {
|
|
124
|
+
const hashed = hashFilename(item[hashKey]);
|
|
125
|
+
const existingItem = await load(hashed);
|
|
126
|
+
if (existingItem) {
|
|
127
|
+
throw new ConflictError(`Item with ${String(hashKey)} "${item[hashKey]}" already exists`);
|
|
128
|
+
}
|
|
129
|
+
const path = await filePath(hashed);
|
|
130
|
+
await mkTableDir;
|
|
131
|
+
const createdItem = await new Promise((resolve, reject) => {
|
|
132
|
+
writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
|
|
133
|
+
});
|
|
134
|
+
successful.push(createdItem);
|
|
135
|
+
// Only call individual afterWrite if batchAfterWrite is not defined
|
|
136
|
+
if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
|
|
137
|
+
await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdItem));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
failed.push({ item, error: error });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Call batchAfterWrite with all successful items if defined
|
|
145
|
+
if ((options === null || options === void 0 ? void 0 : options.batchAfterWrite) && successful.length > 0) {
|
|
146
|
+
await options.batchAfterWrite(successful);
|
|
147
|
+
}
|
|
148
|
+
return { successful, failed };
|
|
149
|
+
},
|
|
102
150
|
};
|
|
103
151
|
};
|
|
@@ -23,6 +23,9 @@ export const openSearchService = (initializer = {}) => (configProvider) => {
|
|
|
23
23
|
region: await resolveConfigValue(config.region),
|
|
24
24
|
service: 'es',
|
|
25
25
|
}),
|
|
26
|
+
maxRetries: 3,
|
|
27
|
+
requestTimeout: 3000,
|
|
28
|
+
pingTimeout: 1000,
|
|
26
29
|
node: await resolveConfigValue(config.node),
|
|
27
30
|
}));
|
|
28
31
|
return (indexConfig) => {
|