thatcher 1.0.35 → 1.0.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "A config-driven application framework for building data-intensive web apps without code.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -284,9 +284,15 @@ export class Thatcher {
284
284
  return create(entity, data, user);
285
285
  }
286
286
 
287
- async update(entity, id, data, user) {
287
+ // opts.expectedVersion: optional optimistic-concurrency guard (see
288
+ // busybase-store.js update()) -- a caller that read the row's _version can
289
+ // pass it here to detect (via a thrown {code:'conflict'} error) a concurrent
290
+ // writer landing in between, instead of silently clobbering it. `user` stays
291
+ // its own positional param (unused by the store today, kept for API
292
+ // stability with existing callers); opts is a new, optional 5th param.
293
+ async update(entity, id, data, user, opts = {}) {
288
294
  const { update } = await import(resolveModule('./lib/busybase-store.js'));
289
- return update(entity, id, data, user);
295
+ return update(entity, id, data, opts);
290
296
  }
291
297
 
292
298
  async delete(entity, id) {
@@ -254,15 +254,42 @@ export async function create(entity, data, user) {
254
254
  return record;
255
255
  }
256
256
 
257
- export async function update(entity, id, data) {
257
+ // opts.expectedVersion: optional optimistic-concurrency guard. When supplied,
258
+ // the write is conditioned on the row's internal _version counter still
259
+ // matching the value the caller last read (a plain read-then-write otherwise
260
+ // has no way to detect a concurrent writer landing in between -- whichever
261
+ // caller writes second wins outright, silently discarding the first caller's
262
+ // change). A stale expectedVersion throws a distinguishable 'conflict' error
263
+ // instead of clobbering; the caller re-reads and retries or surfaces the
264
+ // conflict. _version increments on every write regardless of whether the
265
+ // guard is used, so a caller can always read the CURRENT _version to guard
266
+ // its NEXT write. A dedicated counter, not updated_at, because updated_at
267
+ // (now(), second-precision epoch) can collide within the same second under a
268
+ // genuine fast race, silently defeating the guard exactly when it matters
269
+ // most; _version is a plain integer increment with no precision ceiling.
270
+ // Backward compatible: omitting opts.expectedVersion is the exact prior
271
+ // unconditional-write behaviour, unchanged for every existing caller; _version
272
+ // is added as a new column, ignored by every reader that doesn't ask for it.
273
+ export async function update(entity, id, data, opts = {}) {
258
274
  const spec = specOf(entity);
259
275
  const tbl = tableName(entity);
260
276
 
261
277
  const existing = await get(entity, id);
262
278
  if (!existing) throw new Error(`${entity} with id ${id} not found`);
263
279
 
264
- const patch = { ...data, updated_at: now() };
265
- unwrap(await client().from(tbl).update(patch).eq('id', id), 'update');
280
+ const currentVersion = Number(existing._version) || 0;
281
+ const patch = { ...data, updated_at: now(), _version: currentVersion + 1 };
282
+ let builder = client().from(tbl).update(patch).eq('id', id);
283
+ if (opts.expectedVersion != null) {
284
+ builder = builder.eq('_version', opts.expectedVersion);
285
+ }
286
+ const { data: rows, error } = await builder;
287
+ if (error) throw new Error(`BusyBase update failed: ${error.message || error}`);
288
+ if (opts.expectedVersion != null && Array.isArray(rows) && rows.length === 0) {
289
+ const conflictErr = new Error(`${entity} ${id} was modified by another writer since it was last read`);
290
+ conflictErr.code = 'conflict';
291
+ throw conflictErr;
292
+ }
266
293
  return get(entity, id);
267
294
  }
268
295