@openstax/ts-utils 1.48.1 → 1.50.0

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.
@@ -44,5 +44,3 @@ export * from './resolveConfigValue.js';
44
44
  export declare const stubConfig: <V extends ConfigValue>(configValue: V) => ConfigValueProvider<V>;
45
45
  export * from './envConfig.js';
46
46
  export * from './replaceConfig.js';
47
- export * from './awsParameterConfig.js';
48
- export * from './lambdaParameterConfig.js';
@@ -13,5 +13,3 @@ export * from './resolveConfigValue.js';
13
13
  export const stubConfig = (configValue) => configValue;
14
14
  export * from './envConfig.js';
15
15
  export * from './replaceConfig.js';
16
- export * from './awsParameterConfig.js';
17
- export * from './lambdaParameterConfig.js';
@@ -18,6 +18,7 @@ export declare const dynamoUnversionedDocumentStore: <C extends string = "dynamo
18
18
  getItem: (id: T[K]) => Promise<T | undefined>;
19
19
  incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
20
20
  patchItem: (item: Partial<T>) => Promise<T>;
21
+ conditionalPatchItem: (item: Partial<T>, condition: Partial<T>) => Promise<T>;
21
22
  putItem: (item: T) => Promise<T>;
22
23
  createItem: (item: T) => Promise<T>;
23
24
  batchCreateItem: (items: T[], concurrency?: number) => Promise<{
@@ -113,7 +113,7 @@ export const dynamoUnversionedDocumentStore = (initializer) => {
113
113
  if (!id) {
114
114
  throw new Error(`Key attribute "${key}" is required for patchItem`);
115
115
  }
116
- const entries = Object.entries(item).filter(([field]) => field !== key);
116
+ const entries = Object.entries(item).filter(([field, value]) => field !== key && value !== undefined);
117
117
  if (entries.length === 0) {
118
118
  throw new Error('No attributes to update');
119
119
  }
@@ -147,6 +147,68 @@ export const dynamoUnversionedDocumentStore = (initializer) => {
147
147
  new NotFoundError(`Item with ${key} "${id}" does not exist`) : error;
148
148
  });
149
149
  },
150
+ /* atomically patches only if the condition fields match; throws ConflictError if condition fails */
151
+ conditionalPatchItem: async (item, condition) => {
152
+ const id = item[hashKey];
153
+ const key = hashKey.toString();
154
+ if (!id) {
155
+ throw new Error(`Key attribute "${key}" is required for conditionalPatchItem`);
156
+ }
157
+ const entries = Object.entries(item).filter(([field, value]) => field !== key && value !== undefined);
158
+ if (entries.length === 0) {
159
+ throw new Error('No attributes to update');
160
+ }
161
+ const conditionEntries = Object.entries(condition);
162
+ if (conditionEntries.length === 0) {
163
+ throw new Error('condition must have at least one attribute');
164
+ }
165
+ const updates = [];
166
+ const expressionAttributeNames = { '#k': key };
167
+ const expressionAttributeValues = {};
168
+ entries.forEach(([field, value], index) => {
169
+ updates.push(`#f${index} = :f${index}`);
170
+ expressionAttributeNames[`#f${index}`] = field;
171
+ expressionAttributeValues[`:f${index}`] = encodeDynamoAttribute(value);
172
+ });
173
+ const conditionClauses = ['attribute_exists(#k)'];
174
+ conditionEntries.forEach(([field, value], index) => {
175
+ expressionAttributeNames[`#c${index}`] = field;
176
+ if (value === undefined) {
177
+ conditionClauses.push(`attribute_not_exists(#c${index})`);
178
+ }
179
+ else {
180
+ conditionClauses.push(`#c${index} = :c${index}`);
181
+ expressionAttributeValues[`:c${index}`] = encodeDynamoAttribute(value);
182
+ }
183
+ });
184
+ const cmd = new UpdateItemCommand({
185
+ Key: { [key]: encodeDynamoAttribute(id) },
186
+ TableName: await tableName(),
187
+ UpdateExpression: `SET ${updates.join(', ')}`,
188
+ ConditionExpression: conditionClauses.join(' AND '),
189
+ ExpressionAttributeNames: expressionAttributeNames,
190
+ ExpressionAttributeValues: expressionAttributeValues,
191
+ ReturnValues: 'ALL_NEW',
192
+ ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
193
+ });
194
+ return dynamodb().send(cmd).then(async (result) => {
195
+ var _a;
196
+ if (!result.Attributes) {
197
+ throw new NotFoundError(`Item with ${key} "${id}" does not exist`);
198
+ }
199
+ const updatedDoc = decodeDynamoDocument(result.Attributes);
200
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, updatedDoc));
201
+ return updatedDoc;
202
+ }).catch((error) => {
203
+ if (error.name === 'ConditionalCheckFailedException') {
204
+ if (!error.Item || Object.keys(error.Item).length === 0) {
205
+ throw new NotFoundError(`Item with ${key} "${id}" does not exist`);
206
+ }
207
+ throw new ConflictError(`Condition failed for item with ${key} "${id}"`);
208
+ }
209
+ throw error;
210
+ });
211
+ },
150
212
  /* replaces the entire document with the given data */
151
213
  putItem: async (item) => {
152
214
  var _a;
@@ -18,6 +18,7 @@ export declare const fileSystemUnversionedDocumentStore: <C extends string = "fi
18
18
  getItem: (id: T[K]) => Promise<T | undefined>;
19
19
  incrementItemAttribute: (id: T[K], attribute: keyof T) => Promise<number>;
20
20
  patchItem: (item: Partial<T>) => Promise<T>;
21
+ conditionalPatchItem: (item: Partial<T>, condition: Partial<T>) => Promise<T>;
21
22
  putItem: (item: T) => Promise<T>;
22
23
  createItem: (item: T) => Promise<T>;
23
24
  batchCreateItem: (items: T[], _concurrency?: number) => Promise<{
@@ -62,6 +62,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
62
62
  return load(hashFilename(id));
63
63
  },
64
64
  incrementItemAttribute: async (id, attribute) => {
65
+ var _a;
65
66
  const filename = hashFilename(id);
66
67
  const path = await filePath(filename);
67
68
  await mkTableDir;
@@ -71,19 +72,50 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
71
72
  }
72
73
  const newValue = typeof data[attribute] === 'number' ? data[attribute] + 1 : 1;
73
74
  const newItem = { ...data, [hashKey]: id, [attribute]: newValue };
74
- return new Promise((resolve, reject) => {
75
- writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newValue));
75
+ await new Promise((resolve, reject) => {
76
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
76
77
  });
78
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
79
+ return newValue;
77
80
  },
78
81
  patchItem: async (item) => {
82
+ var _a;
79
83
  const id = item[hashKey];
80
84
  if (!id) {
81
85
  throw new Error(`Key attribute "${hashKey.toString()}" is required for patchItem`);
82
86
  }
83
87
  // This check is just to make this adapter consistent with the dynamo adapter
84
- if (Object.keys(item).filter((key) => key !== hashKey.toString()).length === 0) {
88
+ const definedEntries = Object.entries(item).filter(([k, v]) => k !== hashKey.toString() && v !== undefined);
89
+ if (definedEntries.length === 0) {
90
+ throw new Error('No attributes to update');
91
+ }
92
+ const filename = hashFilename(id);
93
+ const path = await filePath(filename);
94
+ await mkTableDir;
95
+ const data = await load(filename);
96
+ if (!data) {
97
+ throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
98
+ }
99
+ const newItem = { ...data, ...Object.fromEntries(definedEntries) };
100
+ await new Promise((resolve, reject) => {
101
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
102
+ });
103
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
104
+ return newItem;
105
+ },
106
+ conditionalPatchItem: async (item, condition) => {
107
+ var _a;
108
+ const id = item[hashKey];
109
+ if (!id) {
110
+ throw new Error(`Key attribute "${hashKey.toString()}" is required for conditionalPatchItem`);
111
+ }
112
+ const definedEntries = Object.entries(item).filter(([k, v]) => k !== hashKey.toString() && v !== undefined);
113
+ if (definedEntries.length === 0) {
85
114
  throw new Error('No attributes to update');
86
115
  }
116
+ if (Object.keys(condition).length === 0) {
117
+ throw new Error('condition must have at least one attribute');
118
+ }
87
119
  const filename = hashFilename(id);
88
120
  const path = await filePath(filename);
89
121
  await mkTableDir;
@@ -91,19 +123,31 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
91
123
  if (!data) {
92
124
  throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
93
125
  }
94
- const newItem = { ...data, ...item };
95
- return new Promise((resolve, reject) => {
96
- writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
126
+ // The local version of this method currently supports only primitive value conditions
127
+ for (const [key, value] of Object.entries(condition)) {
128
+ if (data[key] !== value) {
129
+ throw new ConflictError(`Condition failed for item with ${hashKey.toString()} "${id}"`);
130
+ }
131
+ }
132
+ const newItem = { ...data, ...Object.fromEntries(definedEntries) };
133
+ await new Promise((resolve, reject) => {
134
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
97
135
  });
136
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
137
+ return newItem;
98
138
  },
99
139
  putItem: async (item) => {
140
+ var _a;
100
141
  const path = await filePath(hashFilename(item[hashKey]));
101
142
  await mkTableDir;
102
- return new Promise((resolve, reject) => {
103
- writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
143
+ await new Promise((resolve, reject) => {
144
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve());
104
145
  });
146
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
147
+ return item;
105
148
  },
106
149
  createItem: async (item) => {
150
+ var _a;
107
151
  const hashed = hashFilename(item[hashKey]);
108
152
  const existingItem = await load(hashed);
109
153
  if (existingItem) {
@@ -111,9 +155,11 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
111
155
  }
112
156
  const path = await filePath(hashed);
113
157
  await mkTableDir;
114
- return new Promise((resolve, reject) => {
115
- writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
158
+ await new Promise((resolve, reject) => {
159
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve());
116
160
  });
161
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
162
+ return item;
117
163
  },
118
164
  batchCreateItem: async (items, _concurrency = 1) => {
119
165
  var _a;
@@ -135,11 +181,11 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
135
181
  const createdItem = await new Promise((resolve, reject) => {
136
182
  writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
137
183
  });
138
- successful.push(createdItem);
139
184
  // Only call individual afterWrite if batchAfterWrite is not defined
140
185
  if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
141
186
  await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdItem));
142
187
  }
188
+ successful.push(createdItem);
143
189
  }
144
190
  catch (error) {
145
191
  failed.push({ item, error: error });