Skip to content

Commit 792ced5

Browse files
committed
[middleware] Upward cascade exception
1 parent e0d5999 commit 792ced5

6 files changed

Lines changed: 247 additions & 52 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ node_modules
77
npm-debug.log
88
.partykit
99
.vscode
10-
tinybase.code-workspace
10+
*.code-workspace
11+
*.tsbuildinfo

site/guides/04_using_middleware.md

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ value. The meaning of this return value depends on the type of callback:
4545

4646
The full list of `willSet*` callbacks you can register is as follows:
4747

48-
| Callback | Parameters | Called | Return |
49-
| ---------------- | ---------------------------- | ---------------------------- | -------------------- |
50-
| willSetContent | content | When setContent is called. | Content or undefined |
51-
| willSetTables | tables | When setTables is called. | Tables or undefined |
52-
| willSetTable | tableId, table | When setTable is called. | Table or undefined |
53-
| willSetRow | tableId, rowId, row | When setRow is called. | Row or undefined |
54-
| willSetCell | tableId, rowId, cellId, cell | When setCell is called. | Cell or undefined |
55-
| willSetValues | values | When setValues is called. | Values or undefined |
56-
| willSetValue | valueId, value | When setValue is called. | Value or undefined |
57-
| willApplyChanges | changes | When applyChanges is called. | Changes or undefined |
48+
| Callback | Parameters | Called | Return |
49+
| ---------------- | ---------------------------- | --------------------------------- | -------------------- |
50+
| willSetContent | content | When setContent is called. | Content or undefined |
51+
| willSetTables | tables | When setTables is called. | Tables or undefined |
52+
| willSetTable | tableId, table | When setTable is called. | Table or undefined |
53+
| willSetRow | tableId, rowId, row | When setRow or setCell is called. | Row or undefined |
54+
| willSetCell | tableId, rowId, cellId, cell | When setCell is called. | Cell or undefined |
55+
| willSetValues | values | When setValues is called. | Values or undefined |
56+
| willSetValue | valueId, value | When setValue is called. | Value or undefined |
57+
| willApplyChanges | changes | When applyChanges is called. | Changes or undefined |
5858

5959
Finally, the full list of `willDel*` callbacks you can register is as follows:
6060

@@ -89,42 +89,51 @@ callback cancels the operation, subsequent `willSet*` callbacks will not be
8989
called.
9090

9191
```js
92-
middleware.addWillSetRowCallback((tableId, rowId, row) => {
93-
console.log('Timestamp row');
94-
return {...row, timestamp: Date.now()};
95-
});
96-
middleware.addWillSetRowCallback((tableId, rowId, row) => {
97-
console.log('Cancel setting row');
98-
return undefined;
99-
});
100-
middleware.addWillSetRowCallback((tableId, rowId, row) => {
101-
console.log('Defaulting pet to be alive');
102-
return {...row, alive: true};
103-
});
92+
middleware
93+
.addWillSetRowCallback((_tableId, _rowId, row) => ({...row, step1: true}))
94+
.addWillSetRowCallback((_tableId, _rowId, row) => ({...row, step2: true}));
10495

105-
store.setRow('pets', 'fido', {'species': 'dog'});
106-
// -> 'Timestamp row'
107-
// -> 'Cancel setting row'
108-
// (Callback 3 is not called because Callback 2 cancels the operation)
96+
store.setRow('pets', 'fido', {species: 'dog'});
97+
console.log(store.getRow('pets', 'fido'));
98+
// -> {species: 'dog', step1: true, step2: true}
99+
```
109100

110-
console.log(store.getTable('pets'));
101+
Returning `undefined` from a callback cancels the entire operation, and
102+
subsequent callbacks will not be called:
103+
104+
```js
105+
middleware.addWillSetRowCallback((tableId, _rowId, row) =>
106+
tableId === 'readonly' ? undefined : row,
107+
);
108+
109+
store.setRow('employees', 'alice', {role: 'admin'});
110+
console.log(store.getRow('employees', 'alice'));
111+
// -> {role: 'admin', step1: true, step2: true}
112+
113+
store.setRow('readonly', 'r1', {data: 'test'});
114+
console.log(store.getTable('readonly'));
111115
// -> {}
112116
```
113117

114118
Similarly, if a `willDel*` callback cancels the delete operation, subsequent
115119
`willDel*` callbacks will not be called. In other words, a callback cannot
116120
re-enable a delete operation that has been cancelled by a previous callback.
117121

118-
A less granular operation on the Store (e.g. setting a Table, which will call
119-
`willSetTable`) will also then call more granular callbacks (e.g. `willSetRow`,
120-
`willSetCell`) for each relevant Row and Cell. BUT a more granular operation
121-
(e.g. setting a Cell, which will call `willSetCell`) will NOT call less granular
122-
callbacks (e.g. `willSetRow`, `willSetTable`, `willSetContent` and so on).
122+
In terms of cascading, a less granular operation on the Store (e.g. setting a
123+
Table, which will call `willSetTable`) will also then call more granular
124+
callbacks (e.g. `willSetRow`, `willSetCell`) for each relevant Row and Cell.
125+
126+
BUT there is one important exception to this rule though! Calling `setCell`
127+
will also fire `willSetRow` (receiving existing cells merged with the new cell),
128+
in addition to the `willSetCell` calls for each Cell in the Row. In other words,
129+
The entire resulting Row is applied as though `setRow` had been called with it.
130+
131+
This is to allow for row- or schema-level validation and transformation to be
132+
applied even when only `setCell` calls are being made.
123133

124-
This might seem strange since, in a way, the Row, Table, and Content were
125-
technically being updated. But the key is to think about the actual method that
126-
was called on the Store, and then expect callbacks for only more granular
127-
elements from there.
134+
Note that `setCell` does not fire `willSetTable`, `willSetTables`, or
135+
`willSetContent` though. The 'upwards' cascade only goes as far as `willSetRow`
136+
for `setCell` calls.
128137

129138
## Complex Object Callbacks
130139

src/@types/middleware/docs.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@
7777
* Multiple WillSetRowCallback functions can be registered and they will be
7878
* called sequentially, the Row being updated successively. If any callback
7979
* returns `undefined`, the chain short-circuits and the Row will not be set.
80+
*
81+
* This callback fires both when setRow is called with a whole Row, and when
82+
* setCell is called for a single Cell. When fired from setCell, the Row passed
83+
* to the callback is a prospective row containing the existing Cell values
84+
* merged with the new Cell value. The entire returned Row is then applied as
85+
* though setRow had been called with it: cells present in the returned Row are
86+
* written (each also passing through willSetCell), and cells absent from the
87+
* returned Row that were present in the existing Row will be deleted. Return
88+
* `undefined` to cancel the operation entirely.
8089
* @param tableId The Id of the Table being written to.
8190
* @param rowId The Id of the Row being set.
8291
* @param row The Row object about to be set.
@@ -490,6 +499,15 @@
490499
* write. Multiple callbacks can be registered and they are called
491500
* sequentially, each receiving the (possibly transformed) row from the
492501
* previous callback.
502+
*
503+
* This callback fires both when a whole Row is set with the setRow method,
504+
* _and_ when a single Cell is set with the setCell method. When called from
505+
* setCell, the callback receives a prospective row consisting of the existing
506+
* Cell values merged with the new Cell value. The entire returned Row is then
507+
* applied as though setRow had been called: all cells in the returned Row are
508+
* written (each also passing through willSetCell), and any cells absent from
509+
* the returned Row that existed before will be deleted. Return `undefined` to
510+
* cancel the operation entirely.
493511
* @param callback The WillSetRowCallback to register.
494512
* @returns A reference to the Middleware object, for chaining.
495513
* @example
@@ -539,6 +557,32 @@
539557
*
540558
* middleware.destroy();
541559
* ```
560+
* @example
561+
* This example shows the callback firing when setCell is called,
562+
* automatically stamping an 'updated' timestamp onto the row each time any
563+
* cell in it is written.
564+
*
565+
* ```js
566+
* import {createMiddleware, createStore} from 'tinybase';
567+
*
568+
* const store = createStore();
569+
* const middleware = createMiddleware(store);
570+
*
571+
* store.setRow('pets', 'fido', {species: 'dog'});
572+
*
573+
* middleware.addWillSetRowCallback((_tableId, _rowId, row) => ({
574+
* ...row,
575+
* updated: Date.now(),
576+
* }));
577+
*
578+
* store.setCell('pets', 'fido', 'legs', 4);
579+
* console.log(store.getCell('pets', 'fido', 'legs'));
580+
* // -> 4
581+
* console.log(typeof store.getCell('pets', 'fido', 'updated'));
582+
* // -> 'number'
583+
*
584+
* middleware.destroy();
585+
* ```
542586
* @category Configuration
543587
* @since v8.0.0
544588
*/

src/middleware/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export const createMiddleware = getCreateFunction(
178178
willDelValues,
179179
willDelValue,
180180
willApplyChanges,
181+
() => willSetRowCallbacks.length > 0,
181182
);
182183

183184
return middleware;

src/store/index.ts

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,15 @@ type ProtectedMethods = [
189189
willDelValues: () => boolean,
190190
willDelValue: (valueId: Id) => boolean,
191191
willApplyChanges: (changes: Changes) => Changes | undefined,
192+
hasWillSetRowCallbacks: () => boolean,
192193
) => void,
193194
setOrDelCell: (
194195
tableId: Id,
195196
rowId: Id,
196197
cellId: Id,
197198
cell: CellOrUndefined,
198199
skipMiddleware?: boolean,
200+
skipRowMiddleware?: boolean,
199201
) => Store,
200202
setOrDelValue: (
201203
valueId: Id,
@@ -258,6 +260,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
258260
willDelValues?: () => boolean,
259261
willDelValue?: (valueId: Id) => boolean,
260262
willApplyChanges?: (changes: Changes) => Changes | undefined,
263+
hasWillSetRowCallbacks?: () => boolean,
261264
] = [];
262265
let internalListeners: [
263266
preStartTransaction?: () => void,
@@ -573,10 +576,18 @@ export const createStore: typeof createStoreDecl = (): Store => {
573576
cellId: Id,
574577
cell: CellOrUndefined,
575578
skipMiddleware?: boolean,
579+
skipRowMiddleware?: boolean,
576580
) =>
577581
isUndefined(cell)
578582
? delCell(tableId, rowId, cellId, true, skipMiddleware)
579-
: setCell(tableId, rowId, cellId, cell, skipMiddleware);
583+
: setCell(
584+
tableId,
585+
rowId,
586+
cellId,
587+
cell,
588+
skipMiddleware,
589+
skipRowMiddleware,
590+
);
580591

581592
const setOrDelValues = (values: Values) =>
582593
objIsEmpty(values) ? delValues() : setValues(values);
@@ -697,6 +708,37 @@ export const createStore: typeof createStoreDecl = (): Store => {
697708
objIsEqual,
698709
) as RowMap;
699710

711+
const applyRowDirectly = (
712+
tableId: Id,
713+
tableMap: TableMap,
714+
rowId: Id,
715+
row: Row,
716+
skipMiddleware?: boolean,
717+
): void => {
718+
mapMatch(
719+
mapEnsure(tableMap, rowId, () => {
720+
rowIdsChanged(tableId, rowId, 1);
721+
return mapNew();
722+
}),
723+
row,
724+
(rowMap, cellId, cell) =>
725+
ifNotUndefined(
726+
getValidatedCell(tableId, rowId, cellId, cell as Cell),
727+
(validCell) =>
728+
setValidCell(
729+
tableId,
730+
rowId,
731+
rowMap,
732+
cellId,
733+
validCell,
734+
skipMiddleware,
735+
),
736+
),
737+
(rowMap, cellId) =>
738+
delValidCell(tableId, tableMap, rowId, rowMap, cellId, true),
739+
);
740+
};
741+
700742
const setValidCell = (
701743
tableId: Id,
702744
rowId: Id,
@@ -1556,6 +1598,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
15561598
cellId: Id,
15571599
cell: Cell | MapCell,
15581600
skipMiddleware?: boolean,
1601+
skipRowMiddleware?: boolean,
15591602
): Store =>
15601603
fluentTransaction(
15611604
(tableId, rowId, cellId) =>
@@ -1566,15 +1609,49 @@ export const createStore: typeof createStoreDecl = (): Store => {
15661609
cellId,
15671610
isFunction(cell) ? cell(getCell(tableId, rowId, cellId)) : cell,
15681611
),
1569-
(validCell) =>
1570-
setCellIntoNewRow(
1571-
tableId,
1572-
getOrCreateTable(tableId),
1573-
rowId,
1574-
cellId,
1575-
validCell,
1576-
skipMiddleware,
1577-
),
1612+
(validCell) => {
1613+
const tableMap = getOrCreateTable(tableId);
1614+
ifNotUndefined(
1615+
skipMiddleware || skipRowMiddleware || !middleware[14]?.()
1616+
? undefined
1617+
: middleware[3],
1618+
(willSetRow) => {
1619+
const existingRowMap = mapGet(tableMap, rowId);
1620+
const prospectiveRow: Row = {
1621+
...(existingRowMap
1622+
? mapToObj<Cell>(existingRowMap)
1623+
: {}),
1624+
[cellId]: validCell,
1625+
};
1626+
ifNotUndefined(
1627+
whileMutating(() =>
1628+
willSetRow(
1629+
tableId,
1630+
rowId,
1631+
structuredClone(prospectiveRow),
1632+
),
1633+
),
1634+
(row) =>
1635+
applyRowDirectly(
1636+
tableId,
1637+
tableMap,
1638+
rowId,
1639+
row,
1640+
skipMiddleware,
1641+
),
1642+
);
1643+
},
1644+
() =>
1645+
setCellIntoNewRow(
1646+
tableId,
1647+
tableMap,
1648+
rowId,
1649+
cellId,
1650+
validCell,
1651+
skipMiddleware,
1652+
),
1653+
);
1654+
},
15781655
),
15791656
tableId,
15801657
rowId,
@@ -1636,6 +1713,8 @@ export const createStore: typeof createStoreDecl = (): Store => {
16361713
rowId,
16371714
cellId,
16381715
cell as CellOrUndefined,
1716+
undefined,
1717+
true,
16391718
),
16401719
),
16411720
),
@@ -2042,6 +2121,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
20422121
willDelValues: () => boolean,
20432122
willDelValue: (valueId: Id) => boolean,
20442123
willApplyChanges: (changes: Changes) => Changes | undefined,
2124+
hasWillSetRowCallbacks: () => boolean,
20452125
) =>
20462126
(middleware = [
20472127
willSetContent,
@@ -2058,6 +2138,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
20582138
willDelValues,
20592139
willDelValue,
20602140
willApplyChanges,
2141+
hasWillSetRowCallbacks,
20612142
]);
20622143

20632144
const setInternalListeners = (

0 commit comments

Comments
 (0)