Skip to content

Commit 2f066ad

Browse files
committed
[middleware] WillDelTableCallback
1 parent 82d825a commit 2f066ad

8 files changed

Lines changed: 291 additions & 68 deletions

File tree

‎src/@types/middleware/docs.js‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@
127127
* @since v8.0.0
128128
*/
129129
/// WillDelCellCallback
130+
/**
131+
* The WillDelTableCallback type describes a function that is called before a
132+
* Table is deleted from the Store.
133+
*
134+
* The callback receives the table Id of the Table about to be deleted. It
135+
* returns `true` to allow the deletion, or `false` to prevent it.
136+
*
137+
* Multiple WillDelTableCallback functions can be registered and they will be
138+
* called sequentially. If any callback returns `false`, the chain
139+
* short-circuits and the Table will not be deleted.
140+
* @param tableId The Id of the Table being deleted.
141+
* @returns `true` to allow the deletion, `false` to prevent it.
142+
* @category Callback
143+
* @since v8.0.0
144+
*/
145+
/// WillDelTableCallback
130146
/**
131147
* The WillDelRowCallback type describes a function that is called before a
132148
* Row is deleted from the Store.
@@ -533,6 +549,39 @@
533549
* @since v8.0.0
534550
*/
535551
/// Middleware.addWillDelCellCallback
552+
/**
553+
* The addWillDelTableCallback method registers a WillDelTableCallback that
554+
* will be called before any Table is deleted from the Store.
555+
*
556+
* The callback returns `true` to allow the deletion or `false` to prevent
557+
* it. Multiple callbacks can be registered and they are called sequentially.
558+
* If any callback returns `false`, the deletion is prevented.
559+
* @param callback The WillDelTableCallback to register.
560+
* @returns A reference to the Middleware object, for chaining.
561+
* @example
562+
* This example registers a callback that prevents deleting the 'pets' table
563+
* from the pet store.
564+
*
565+
* ```js
566+
* import {createMiddleware, createStore} from 'tinybase';
567+
*
568+
* const store = createStore();
569+
* const middleware = createMiddleware(store);
570+
*
571+
* store.setTable('pets', {fido: {species: 'dog'}, felix: {species: 'cat'}});
572+
*
573+
* middleware.addWillDelTableCallback((tableId) => tableId !== 'pets');
574+
*
575+
* store.delTable('pets');
576+
* console.log(store.getTable('pets'));
577+
* // -> {fido: {species: 'dog'}, felix: {species: 'cat'}}
578+
*
579+
* middleware.destroy();
580+
* ```
581+
* @category Configuration
582+
* @since v8.0.0
583+
*/
584+
/// Middleware.addWillDelTableCallback
536585
/**
537586
* The addWillDelRowCallback method registers a WillDelRowCallback that will
538587
* be called before any Row is deleted from the Store.

‎src/@types/middleware/index.d.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export type WillDelCellCallback = (
4848
cellId: Id,
4949
) => boolean;
5050

51+
/// WillDelTableCallback
52+
export type WillDelTableCallback = (tableId: Id) => boolean;
53+
5154
/// WillDelRowCallback
5255
export type WillDelRowCallback = (tableId: Id, rowId: Id) => boolean;
5356

@@ -80,6 +83,9 @@ export interface Middleware {
8083
/// Middleware.addWillDelCellCallback
8184
addWillDelCellCallback(callback: WillDelCellCallback): Middleware;
8285

86+
/// Middleware.addWillDelTableCallback
87+
addWillDelTableCallback(callback: WillDelTableCallback): Middleware;
88+
8389
/// Middleware.addWillDelRowCallback
8490
addWillDelRowCallback(callback: WillDelRowCallback): Middleware;
8591

‎src/@types/middleware/with-schemas/index.d.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ export type WillDelCellCallback<
9595
...params: Params | [tableId: never, rowId: never, cellId: never]
9696
) => boolean;
9797

98+
/// WillDelTableCallback
99+
export type WillDelTableCallback<
100+
Schema extends OptionalTablesSchema,
101+
Params extends any[] = TableIdFromSchema<Schema> extends infer TableId
102+
? TableId extends TableIdFromSchema<Schema>
103+
? [tableId: TableId]
104+
: never
105+
: never,
106+
> = (...params: Params | [tableId: never]) => boolean;
107+
98108
/// WillDelRowCallback
99109
export type WillDelRowCallback<
100110
Schema extends OptionalTablesSchema,
@@ -153,6 +163,11 @@ export interface Middleware<in out Schemas extends OptionalSchemas> {
153163
callback: WillDelCellCallback<Schemas[0]>,
154164
): Middleware<Schemas>;
155165

166+
/// Middleware.addWillDelTableCallback
167+
addWillDelTableCallback(
168+
callback: WillDelTableCallback<Schemas[0]>,
169+
): Middleware<Schemas>;
170+
156171
/// Middleware.addWillDelRowCallback
157172
addWillDelRowCallback(
158173
callback: WillDelRowCallback<Schemas[0]>,

‎src/middleware/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
Middleware,
44
WillDelCellCallback,
55
WillDelRowCallback,
6+
WillDelTableCallback,
67
WillDelValueCallback,
78
WillDelValuesCallback,
89
WillSetCellCallback,
@@ -35,6 +36,7 @@ export const createMiddleware = getCreateFunction(
3536
const willSetValueCallbacks: WillSetValueCallback[] = [];
3637
const willSetValuesCallbacks: WillSetValuesCallback[] = [];
3738
const willDelCellCallbacks: WillDelCellCallback[] = [];
39+
const willDelTableCallbacks: WillDelTableCallback[] = [];
3840
const willDelRowCallbacks: WillDelRowCallback[] = [];
3941
const willDelValueCallbacks: WillDelValueCallback[] = [];
4042
const willDelValuesCallbacks: WillDelValuesCallback[] = [];
@@ -101,6 +103,9 @@ export const createMiddleware = getCreateFunction(
101103
const willDelRow = (tableId: Id, rowId: Id): boolean =>
102104
arrayEvery(willDelRowCallbacks, (callback) => callback(tableId, rowId));
103105

106+
const willDelTable = (tableId: Id): boolean =>
107+
arrayEvery(willDelTableCallbacks, (callback) => callback(tableId));
108+
104109
const willDelValue = (valueId: Id): boolean =>
105110
arrayEvery(willDelValueCallbacks, (callback) => callback(valueId));
106111

@@ -117,6 +122,7 @@ export const createMiddleware = getCreateFunction(
117122
willDelRow,
118123
willDelValues,
119124
willSetTable,
125+
willDelTable,
120126
);
121127

122128
const getStore = (): Store => store;
@@ -153,6 +159,11 @@ export const createMiddleware = getCreateFunction(
153159
const addWillDelRowCallback = (callback: WillDelRowCallback): Middleware =>
154160
fluent(() => arrayPush(willDelRowCallbacks, callback));
155161

162+
const addWillDelTableCallback = (
163+
callback: WillDelTableCallback,
164+
): Middleware =>
165+
fluent(() => arrayPush(willDelTableCallbacks, callback));
166+
156167
const addWillDelValueCallback = (
157168
callback: WillDelValueCallback,
158169
): Middleware => fluent(() => arrayPush(willDelValueCallbacks, callback));
@@ -172,6 +183,7 @@ export const createMiddleware = getCreateFunction(
172183
addWillSetValueCallback,
173184
addWillSetValuesCallback,
174185
addWillDelCellCallback,
186+
addWillDelTableCallback,
175187
addWillDelRowCallback,
176188
addWillDelValueCallback,
177189
addWillDelValuesCallback,

‎src/store/index.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
177177
willDelRow?: (tableId: Id, rowId: Id) => boolean,
178178
willDelValues?: () => boolean,
179179
willSetTable?: (tableId: Id, table: Table) => Table | undefined,
180+
willDelTable?: (tableId: Id) => boolean,
180181
] = [];
181182
let internalListeners: [
182183
preStartTransaction?: () => void,
@@ -629,8 +630,12 @@ export const createStore: typeof createStoreDecl = (): Store => {
629630
const getOrCreateTable = (tableId: Id) =>
630631
mapGet(tablesMap, tableId) ?? setValidTable(tableId, {});
631632

632-
const delValidTable = (tableId: Id): TableMap =>
633-
setValidTable(tableId, {}, true);
633+
const delValidTable = (tableId: Id): TableMap => {
634+
if (internalWillSets[9]?.(tableId) ?? true) {
635+
return setValidTable(tableId, {}, true);
636+
}
637+
return mapGet(tablesMap, tableId) as TableMap;
638+
};
634639

635640
const delValidRow = (tableId: Id, tableMap: TableMap, rowId: Id): void => {
636641
if (internalWillSets[6]?.(tableId, rowId) ?? true) {
@@ -1802,6 +1807,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
18021807
willDelRow: (tableId: Id, rowId: Id) => boolean,
18031808
willDelValues: () => boolean,
18041809
willSetTable: (tableId: Id, table: Table) => Table | undefined,
1810+
willDelTable: (tableId: Id) => boolean,
18051811
) =>
18061812
(internalWillSets = [
18071813
willSetCell,
@@ -1813,6 +1819,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
18131819
willDelRow,
18141820
willDelValues,
18151821
willSetTable,
1822+
willDelTable,
18161823
]);
18171824

18181825
const setInternalListeners = (

‎test/unit/core/other/middleware.test.ts‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,173 @@ describe('willDelRow', () => {
14081408
});
14091409
});
14101410

1411+
describe('willDelTable', () => {
1412+
describe('allow', () => {
1413+
test('returning true allows delete', () => {
1414+
middleware.addWillDelTableCallback(() => true);
1415+
store.setTable('t1', {r1: {c1: 'a'}});
1416+
store.delTable('t1');
1417+
expect(store.getTable('t1')).toEqual({});
1418+
expect(store.getTables()).toEqual({});
1419+
});
1420+
});
1421+
1422+
describe('block', () => {
1423+
test('returning false blocks delete', () => {
1424+
middleware.addWillDelTableCallback(() => false);
1425+
store.setTable('t1', {r1: {c1: 'a'}});
1426+
store.delTable('t1');
1427+
expect(store.getTable('t1')).toEqual({r1: {c1: 'a'}});
1428+
});
1429+
1430+
test('conditionally blocks', () => {
1431+
middleware.addWillDelTableCallback((tableId) => {
1432+
return tableId !== 'protected';
1433+
});
1434+
store.setTables({
1435+
t1: {r1: {c1: 'a'}},
1436+
protected: {r1: {c1: 'b'}},
1437+
});
1438+
store.delTable('t1');
1439+
store.delTable('protected');
1440+
expect(store.getTable('t1')).toEqual({});
1441+
expect(store.getTable('protected')).toEqual({r1: {c1: 'b'}});
1442+
});
1443+
});
1444+
1445+
describe('chaining', () => {
1446+
test('all must return true to allow', () => {
1447+
middleware
1448+
.addWillDelTableCallback(() => true)
1449+
.addWillDelTableCallback(() => true);
1450+
store.setTable('t1', {r1: {c1: 'a'}});
1451+
store.delTable('t1');
1452+
expect(store.getTables()).toEqual({});
1453+
});
1454+
1455+
test('any returning false blocks', () => {
1456+
middleware
1457+
.addWillDelTableCallback(() => true)
1458+
.addWillDelTableCallback(() => false);
1459+
store.setTable('t1', {r1: {c1: 'a'}});
1460+
store.delTable('t1');
1461+
expect(store.getTable('t1')).toEqual({r1: {c1: 'a'}});
1462+
});
1463+
1464+
test('first blocks, second never called', () => {
1465+
const secondCalled = vi.fn();
1466+
middleware
1467+
.addWillDelTableCallback(() => false)
1468+
.addWillDelTableCallback((...args) => {
1469+
secondCalled(...args);
1470+
return true;
1471+
});
1472+
store.setTable('t1', {r1: {c1: 'a'}});
1473+
store.delTable('t1');
1474+
expect(secondCalled).not.toHaveBeenCalled();
1475+
expect(store.getTable('t1')).toEqual({r1: {c1: 'a'}});
1476+
});
1477+
});
1478+
1479+
describe('entry points', () => {
1480+
test('called from delTable', () => {
1481+
const calls: string[] = [];
1482+
middleware.addWillDelTableCallback((tableId) => {
1483+
calls.push(tableId);
1484+
return true;
1485+
});
1486+
store.setTable('t1', {r1: {c1: 'a'}});
1487+
store.delTable('t1');
1488+
expect(calls).toEqual(['t1']);
1489+
});
1490+
1491+
test('called from delTables', () => {
1492+
const calls: string[] = [];
1493+
middleware.addWillDelTableCallback((tableId) => {
1494+
calls.push(tableId);
1495+
return true;
1496+
});
1497+
store.setTables({t1: {r1: {c1: 'a'}}, t2: {r1: {c1: 'b'}}});
1498+
store.delTables();
1499+
expect(calls).toEqual(['t1', 't2']);
1500+
});
1501+
1502+
test('not called from delRow', () => {
1503+
const calls: string[] = [];
1504+
middleware.addWillDelTableCallback((tableId) => {
1505+
calls.push(tableId);
1506+
return true;
1507+
});
1508+
store.setTable('t1', {r1: {c1: 'a'}, r2: {c1: 'b'}});
1509+
store.delRow('t1', 'r1');
1510+
expect(calls).toEqual([]);
1511+
expect(store.getTable('t1')).toEqual({r2: {c1: 'b'}});
1512+
});
1513+
1514+
test('block from delTables prevents table deletion', () => {
1515+
middleware.addWillDelTableCallback(() => false);
1516+
store.setTables({t1: {r1: {c1: 'a'}}, t2: {r1: {c1: 'b'}}});
1517+
store.delTables();
1518+
expect(store.getTables()).toEqual({
1519+
t1: {r1: {c1: 'a'}},
1520+
t2: {r1: {c1: 'b'}},
1521+
});
1522+
});
1523+
1524+
test('called when setTables replaces existing tables', () => {
1525+
const delCalls: string[] = [];
1526+
middleware.addWillDelTableCallback((tableId) => {
1527+
delCalls.push(tableId);
1528+
return true;
1529+
});
1530+
store.setTables({t1: {r1: {c1: 'a'}}, t2: {r1: {c1: 'b'}}});
1531+
delCalls.length = 0;
1532+
store.setTables({t1: {r1: {c1: 'A'}}});
1533+
// t2 should be deleted
1534+
expect(delCalls).toEqual(['t2']);
1535+
expect(store.getTables()).toEqual({t1: {r1: {c1: 'A'}}});
1536+
});
1537+
1538+
test('block prevents removal of old tables during setTables', () => {
1539+
middleware.addWillDelTableCallback(() => false);
1540+
store.setTables({t1: {r1: {c1: 'a'}}, t2: {r1: {c1: 'b'}}});
1541+
store.setTables({t1: {r1: {c1: 'A'}}});
1542+
// t2 should be preserved because delete was blocked
1543+
expect(store.getTables()).toEqual({
1544+
t1: {r1: {c1: 'A'}},
1545+
t2: {r1: {c1: 'b'}},
1546+
});
1547+
});
1548+
});
1549+
1550+
describe('interaction with willDelRow', () => {
1551+
test('willDelTable blocks before willDelRow is called', () => {
1552+
const rowCalls = vi.fn();
1553+
middleware.addWillDelTableCallback(() => false);
1554+
middleware.addWillDelRowCallback((...args) => {
1555+
rowCalls(...args);
1556+
return true;
1557+
});
1558+
store.setTable('t1', {r1: {c1: 'a'}});
1559+
store.delTable('t1');
1560+
expect(rowCalls).not.toHaveBeenCalled();
1561+
expect(store.getTable('t1')).toEqual({r1: {c1: 'a'}});
1562+
});
1563+
1564+
test('willDelTable allows, willDelRow still checked', () => {
1565+
const rowCalls: string[] = [];
1566+
middleware.addWillDelTableCallback(() => true);
1567+
middleware.addWillDelRowCallback((tableId, rowId) => {
1568+
rowCalls.push(`${tableId}/${rowId}`);
1569+
return true;
1570+
});
1571+
store.setTable('t1', {r1: {c1: 'a'}, r2: {c1: 'b'}});
1572+
store.delTable('t1');
1573+
expect(rowCalls).toEqual(['t1/r1', 't1/r2']);
1574+
});
1575+
});
1576+
});
1577+
14111578
describe('willDelValue', () => {
14121579
describe('allow', () => {
14131580
test('returning true allows delete', () => {
@@ -1825,6 +1992,11 @@ describe('fluent API', () => {
18251992
expect(result).toBe(middleware);
18261993
});
18271994

1995+
test('addWillDelTableCallback returns middleware', () => {
1996+
const result = middleware.addWillDelTableCallback(() => true);
1997+
expect(result).toBe(middleware);
1998+
});
1999+
18282000
test('addWillDelValueCallback returns middleware', () => {
18292001
const result = middleware.addWillDelValueCallback(() => true);
18302002
expect(result).toBe(middleware);
@@ -1843,6 +2015,7 @@ describe('fluent API', () => {
18432015
.addWillSetValueCallback((_valueId, value) => value)
18442016
.addWillSetValuesCallback((values) => values)
18452017
.addWillDelCellCallback(() => true)
2018+
.addWillDelTableCallback(() => true)
18462019
.addWillDelRowCallback(() => true)
18472020
.addWillDelValueCallback(() => true)
18482021
.addWillDelValuesCallback(() => true);

0 commit comments

Comments
 (0)