@openstax/ts-utils 1.48.2 → 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.
@@ -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<{
@@ -119,7 +119,7 @@ const dynamoUnversionedDocumentStore = (initializer) => {
119
119
  if (!id) {
120
120
  throw new Error(`Key attribute "${key}" is required for patchItem`);
121
121
  }
122
- const entries = Object.entries(item).filter(([field]) => field !== key);
122
+ const entries = Object.entries(item).filter(([field, value]) => field !== key && value !== undefined);
123
123
  if (entries.length === 0) {
124
124
  throw new Error('No attributes to update');
125
125
  }
@@ -153,6 +153,68 @@ const dynamoUnversionedDocumentStore = (initializer) => {
153
153
  new index_js_2.NotFoundError(`Item with ${key} "${id}" does not exist`) : error;
154
154
  });
155
155
  },
156
+ /* atomically patches only if the condition fields match; throws ConflictError if condition fails */
157
+ conditionalPatchItem: async (item, condition) => {
158
+ const id = item[hashKey];
159
+ const key = hashKey.toString();
160
+ if (!id) {
161
+ throw new Error(`Key attribute "${key}" is required for conditionalPatchItem`);
162
+ }
163
+ const entries = Object.entries(item).filter(([field, value]) => field !== key && value !== undefined);
164
+ if (entries.length === 0) {
165
+ throw new Error('No attributes to update');
166
+ }
167
+ const conditionEntries = Object.entries(condition);
168
+ if (conditionEntries.length === 0) {
169
+ throw new Error('condition must have at least one attribute');
170
+ }
171
+ const updates = [];
172
+ const expressionAttributeNames = { '#k': key };
173
+ const expressionAttributeValues = {};
174
+ entries.forEach(([field, value], index) => {
175
+ updates.push(`#f${index} = :f${index}`);
176
+ expressionAttributeNames[`#f${index}`] = field;
177
+ expressionAttributeValues[`:f${index}`] = (0, dynamoEncoding_js_1.encodeDynamoAttribute)(value);
178
+ });
179
+ const conditionClauses = ['attribute_exists(#k)'];
180
+ conditionEntries.forEach(([field, value], index) => {
181
+ expressionAttributeNames[`#c${index}`] = field;
182
+ if (value === undefined) {
183
+ conditionClauses.push(`attribute_not_exists(#c${index})`);
184
+ }
185
+ else {
186
+ conditionClauses.push(`#c${index} = :c${index}`);
187
+ expressionAttributeValues[`:c${index}`] = (0, dynamoEncoding_js_1.encodeDynamoAttribute)(value);
188
+ }
189
+ });
190
+ const cmd = new client_dynamodb_1.UpdateItemCommand({
191
+ Key: { [key]: (0, dynamoEncoding_js_1.encodeDynamoAttribute)(id) },
192
+ TableName: await tableName(),
193
+ UpdateExpression: `SET ${updates.join(', ')}`,
194
+ ConditionExpression: conditionClauses.join(' AND '),
195
+ ExpressionAttributeNames: expressionAttributeNames,
196
+ ExpressionAttributeValues: expressionAttributeValues,
197
+ ReturnValues: 'ALL_NEW',
198
+ ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
199
+ });
200
+ return dynamodb().send(cmd).then(async (result) => {
201
+ var _a;
202
+ if (!result.Attributes) {
203
+ throw new index_js_2.NotFoundError(`Item with ${key} "${id}" does not exist`);
204
+ }
205
+ const updatedDoc = (0, dynamoEncoding_js_1.decodeDynamoDocument)(result.Attributes);
206
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, updatedDoc));
207
+ return updatedDoc;
208
+ }).catch((error) => {
209
+ if (error.name === 'ConditionalCheckFailedException') {
210
+ if (!error.Item || Object.keys(error.Item).length === 0) {
211
+ throw new index_js_2.NotFoundError(`Item with ${key} "${id}" does not exist`);
212
+ }
213
+ throw new index_js_2.ConflictError(`Condition failed for item with ${key} "${id}"`);
214
+ }
215
+ throw error;
216
+ });
217
+ },
156
218
  /* replaces the entire document with the given data */
157
219
  putItem: async (item) => {
158
220
  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<{
@@ -101,6 +101,7 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
101
101
  return load(hashFilename(id));
102
102
  },
103
103
  incrementItemAttribute: async (id, attribute) => {
104
+ var _a;
104
105
  const filename = hashFilename(id);
105
106
  const path = await filePath(filename);
106
107
  await mkTableDir;
@@ -110,19 +111,50 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
110
111
  }
111
112
  const newValue = typeof data[attribute] === 'number' ? data[attribute] + 1 : 1;
112
113
  const newItem = { ...data, [hashKey]: id, [attribute]: newValue };
113
- return new Promise((resolve, reject) => {
114
- writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newValue));
114
+ await new Promise((resolve, reject) => {
115
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
115
116
  });
117
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
118
+ return newValue;
116
119
  },
117
120
  patchItem: async (item) => {
121
+ var _a;
118
122
  const id = item[hashKey];
119
123
  if (!id) {
120
124
  throw new Error(`Key attribute "${hashKey.toString()}" is required for patchItem`);
121
125
  }
122
126
  // This check is just to make this adapter consistent with the dynamo adapter
123
- if (Object.keys(item).filter((key) => key !== hashKey.toString()).length === 0) {
127
+ const definedEntries = Object.entries(item).filter(([k, v]) => k !== hashKey.toString() && v !== undefined);
128
+ if (definedEntries.length === 0) {
129
+ throw new Error('No attributes to update');
130
+ }
131
+ const filename = hashFilename(id);
132
+ const path = await filePath(filename);
133
+ await mkTableDir;
134
+ const data = await load(filename);
135
+ if (!data) {
136
+ throw new index_js_2.NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
137
+ }
138
+ const newItem = { ...data, ...Object.fromEntries(definedEntries) };
139
+ await new Promise((resolve, reject) => {
140
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
141
+ });
142
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
143
+ return newItem;
144
+ },
145
+ conditionalPatchItem: async (item, condition) => {
146
+ var _a;
147
+ const id = item[hashKey];
148
+ if (!id) {
149
+ throw new Error(`Key attribute "${hashKey.toString()}" is required for conditionalPatchItem`);
150
+ }
151
+ const definedEntries = Object.entries(item).filter(([k, v]) => k !== hashKey.toString() && v !== undefined);
152
+ if (definedEntries.length === 0) {
124
153
  throw new Error('No attributes to update');
125
154
  }
155
+ if (Object.keys(condition).length === 0) {
156
+ throw new Error('condition must have at least one attribute');
157
+ }
126
158
  const filename = hashFilename(id);
127
159
  const path = await filePath(filename);
128
160
  await mkTableDir;
@@ -130,19 +162,31 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
130
162
  if (!data) {
131
163
  throw new index_js_2.NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
132
164
  }
133
- const newItem = { ...data, ...item };
134
- return new Promise((resolve, reject) => {
135
- writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
165
+ // The local version of this method currently supports only primitive value conditions
166
+ for (const [key, value] of Object.entries(condition)) {
167
+ if (data[key] !== value) {
168
+ throw new index_js_2.ConflictError(`Condition failed for item with ${hashKey.toString()} "${id}"`);
169
+ }
170
+ }
171
+ const newItem = { ...data, ...Object.fromEntries(definedEntries) };
172
+ await new Promise((resolve, reject) => {
173
+ writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve());
136
174
  });
175
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, newItem));
176
+ return newItem;
137
177
  },
138
178
  putItem: async (item) => {
179
+ var _a;
139
180
  const path = await filePath(hashFilename(item[hashKey]));
140
181
  await mkTableDir;
141
- return new Promise((resolve, reject) => {
142
- writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
182
+ await new Promise((resolve, reject) => {
183
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve());
143
184
  });
185
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
186
+ return item;
144
187
  },
145
188
  createItem: async (item) => {
189
+ var _a;
146
190
  const hashed = hashFilename(item[hashKey]);
147
191
  const existingItem = await load(hashed);
148
192
  if (existingItem) {
@@ -150,9 +194,11 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
150
194
  }
151
195
  const path = await filePath(hashed);
152
196
  await mkTableDir;
153
- return new Promise((resolve, reject) => {
154
- writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
197
+ await new Promise((resolve, reject) => {
198
+ writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve());
155
199
  });
200
+ await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, item));
201
+ return item;
156
202
  },
157
203
  batchCreateItem: async (items, _concurrency = 1) => {
158
204
  var _a;
@@ -174,11 +220,11 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
174
220
  const createdItem = await new Promise((resolve, reject) => {
175
221
  writeFile(path, JSON.stringify(item, null, 2), (err) => err ? reject(err) : resolve(item));
176
222
  });
177
- successful.push(createdItem);
178
223
  // Only call individual afterWrite if batchAfterWrite is not defined
179
224
  if (!(options === null || options === void 0 ? void 0 : options.batchAfterWrite)) {
180
225
  await ((_a = options === null || options === void 0 ? void 0 : options.afterWrite) === null || _a === void 0 ? void 0 : _a.call(options, createdItem));
181
226
  }
227
+ successful.push(createdItem);
182
228
  }
183
229
  catch (error) {
184
230
  failed.push({ item, error: error });