|
20 | 20 | // grid's consistent save stays silent), nor is a key the caller never sent; |
21 | 21 | // - a caller value another strip already took is reported once, under that |
22 | 22 | // strip's reason, never twice; |
23 | | -// - `strictReadonlyWrites` refuses a replaced value, writing nothing — its |
24 | | -// coverage is derived from what `onFieldsDropped` reports. |
| 23 | +// - `strictReadonlyWrites` does NOT refuse a replaced value in 18.x (the |
| 24 | +// maintainer's ruling 「甲」: only the loud half ships; the refusal waits |
| 25 | +// for 19.0) — the write completes with the computed value and the report |
| 26 | +// reaches the listener under strict too, while a strict write that ALSO |
| 27 | +// carries a refusable drop is still refused, and fires no listener. |
25 | 28 |
|
26 | 29 | import { describe, it, expect, beforeEach } from 'vitest'; |
27 | 30 | import type { DroppedFieldsEvent } from '@objectstack/spec/data'; |
@@ -251,38 +254,51 @@ describe('[#24690] a caller value a stored expression replaced is reported as `s |
251 | 254 | }); |
252 | 255 | }); |
253 | 256 |
|
254 | | - describe('strictReadonlyWrites — coverage derived from the reported set refuses a replaced value', () => { |
255 | | - it('update by id: ERR_READONLY_FIELD_REJECTED carrying the `stored_expression` drop, nothing written', async () => { |
256 | | - const err: any = await engine |
257 | | - .update(LINE, { id: 'line_draft', quantity: 4, amount: 5 }, { strictReadonlyWrites: true } as any) |
258 | | - .catch((e: unknown) => e); |
259 | | - expect(err).toBeInstanceOf(ReadonlyFieldRejectedError); |
260 | | - expect(err.code).toBe('ERR_READONLY_FIELD_REJECTED'); |
261 | | - expect(err.drops).toEqual([AMOUNT]); |
262 | | - expect(line('line_draft')).toMatchObject({ quantity: 3, amount: 30 }); |
| 257 | + describe('strictReadonlyWrites — a replaced value is REPORTED, not refused, in 18.x (ruling 「甲」)', () => { |
| 258 | + it('update by id: the write completes with the computed value, and the report reaches the listener', async () => { |
| 259 | + const l = listener(); |
| 260 | + await engine.update(LINE, { id: 'line_draft', quantity: 4, amount: 5 }, { strictReadonlyWrites: true, onFieldsDropped: l.onFieldsDropped } as any); |
| 261 | + expect(line('line_draft')).toMatchObject({ quantity: 4, amount: 40 }); |
| 262 | + expect(l.events).toEqual([AMOUNT]); |
| 263 | + }); |
| 264 | + |
| 265 | + it('insert: the row is written with the computed value, and reported', async () => { |
| 266 | + const l = listener(); |
| 267 | + await engine.insert(LINE, { invoice: 'INV-1004', quantity: 2, unit_price: 10, amount: 999 }, { strictReadonlyWrites: true, onFieldsDropped: l.onFieldsDropped }); |
| 268 | + const rows = [...storeFor(LINE).values()]; |
| 269 | + expect(rows).toHaveLength(4); |
| 270 | + expect(rows.find((r) => r.amount === 20 && r.quantity === 2)).toBeDefined(); |
| 271 | + expect(l.events).toEqual([AMOUNT]); |
| 272 | + }); |
| 273 | + |
| 274 | + it('predicate update: every matched row is written, and the replacement is reported', async () => { |
| 275 | + const l = listener(); |
| 276 | + await engine.update(LINE, { quantity: 2, unit_price: 5, amount: 1 }, { where: { invoice: 'INV-1004' }, multi: true, strictReadonlyWrites: true, onFieldsDropped: l.onFieldsDropped } as any); |
| 277 | + expect([line('line_draft').amount, line('line_draft_2').amount]).toEqual([10, 10]); |
| 278 | + expect(l.events).toEqual([AMOUNT]); |
263 | 279 | }); |
264 | 280 |
|
265 | | - it('insert: refused the same way, before the driver', async () => { |
| 281 | + it('a strict write that ALSO carries a refusable drop is refused for THAT drop alone, writes nothing, and fires no listener', async () => { |
| 282 | + const l = listener(); |
266 | 283 | const err: any = await engine |
267 | | - .insert(LINE, { invoice: 'INV-1004', quantity: 2, unit_price: 10, amount: 999 }, { strictReadonlyWrites: true }) |
| 284 | + .update(LINE, { id: 'line_paid', quantity: 60, amount: 2999.4 }, { strictReadonlyWrites: true, onFieldsDropped: l.onFieldsDropped } as any) |
268 | 285 | .catch((e: unknown) => e); |
269 | 286 | expect(err).toBeInstanceOf(ReadonlyFieldRejectedError); |
270 | | - expect(err.drops).toEqual([AMOUNT]); |
271 | | - expect([...storeFor(LINE).values()]).toHaveLength(3); |
| 287 | + expect(err.code).toBe('ERR_READONLY_FIELD_REJECTED'); |
| 288 | + expect(err.drops).toEqual([{ object: LINE, fields: ['quantity'], reason: 'readonly_when' }]); |
| 289 | + expect(line('line_paid')).toMatchObject({ quantity: 6, amount: 299.94 }); |
| 290 | + expect(l.events).toEqual([]); |
272 | 291 | }); |
273 | 292 |
|
274 | | - it('predicate update: refused before any row is written', async () => { |
| 293 | + it('insert: a strict write with a refusable drop is refused before the computation, carrying no stored_expression drop', async () => { |
| 294 | + const l = listener(); |
275 | 295 | const err: any = await engine |
276 | | - .update(LINE, { quantity: 2, unit_price: 5, amount: 1 }, { where: { invoice: 'INV-1004' }, multi: true, strictReadonlyWrites: true } as any) |
| 296 | + .insert('qa_locked_line', { quantity: 4, unit_price: 2, amount: 5 }, { strictReadonlyWrites: true, onFieldsDropped: l.onFieldsDropped }) |
277 | 297 | .catch((e: unknown) => e); |
278 | 298 | expect(err).toBeInstanceOf(ReadonlyFieldRejectedError); |
279 | | - expect(err.drops).toEqual([AMOUNT]); |
280 | | - expect([line('line_draft').amount, line('line_draft_2').amount]).toEqual([30, 14]); |
281 | | - }); |
282 | | - |
283 | | - it('control: a consistent value is not refused — nothing was replaced', async () => { |
284 | | - await engine.update(LINE, { id: 'line_draft', quantity: 5, unit_price: 20, amount: 100 }, { strictReadonlyWrites: true } as any); |
285 | | - expect(line('line_draft')).toMatchObject({ quantity: 5, amount: 100 }); |
| 299 | + expect(err.drops.map((d: DroppedFieldsEvent) => d.reason)).not.toContain('stored_expression'); |
| 300 | + expect([...storeFor('qa_locked_line').values()]).toHaveLength(1); |
| 301 | + expect(l.events).toEqual([]); |
286 | 302 | }); |
287 | 303 | }); |
288 | 304 | }); |
0 commit comments