Skip to content

Commit 9f60040

Browse files
committed
[middleware] WillDelValuesCallback
1 parent 2e76b3b commit 9f60040

7 files changed

Lines changed: 197 additions & 2 deletions

File tree

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@
141141
* @since v8.0.0
142142
*/
143143
/// WillDelValueCallback
144+
/**
145+
* The WillDelValuesCallback type describes a function that is called before all
146+
* Values are deleted from the Store.
147+
*
148+
* The callback takes no parameters. It returns `true` to allow the deletion, or
149+
* `false` to prevent it.
150+
*
151+
* Multiple WillDelValuesCallback functions can be registered and they will be
152+
* called sequentially. If any callback returns `false`, the chain
153+
* short-circuits and the Values will not be deleted.
154+
* @returns `true` to allow the deletion, `false` to prevent it.
155+
* @category Callback
156+
* @since v8.0.0
157+
*/
158+
/// WillDelValuesCallback
144159
/**
145160
* A Middleware object lets you intercept and validate writes to a Store.
146161
*
@@ -499,6 +514,39 @@
499514
* @since v8.0.0
500515
*/
501516
/// Middleware.addWillDelValueCallback
517+
/**
518+
* The addWillDelValuesCallback method registers a WillDelValuesCallback that
519+
* will be called before all Values are deleted from the Store.
520+
*
521+
* The callback returns `true` to allow the deletion or `false` to prevent it.
522+
* Multiple callbacks can be registered and they are called sequentially. If
523+
* any callback returns `false`, the deletion is prevented.
524+
* @param callback The WillDelValuesCallback to register.
525+
* @returns A reference to the Middleware object, for chaining.
526+
* @example
527+
* This example registers a callback that prevents deleting all Values from
528+
* the pet store.
529+
*
530+
* ```js
531+
* import {createMiddleware, createStore} from 'tinybase';
532+
*
533+
* const store = createStore();
534+
* const middleware = createMiddleware(store);
535+
*
536+
* store.setValues({storeName: 'happy pets', limit: 50});
537+
*
538+
* middleware.addWillDelValuesCallback(() => false);
539+
*
540+
* store.delValues();
541+
* console.log(store.getValues());
542+
* // -> {storeName: 'happy pets', limit: 50}
543+
*
544+
* middleware.destroy();
545+
* ```
546+
* @category Configuration
547+
* @since v8.0.0
548+
*/
549+
/// Middleware.addWillDelValuesCallback
502550
/**
503551
* The destroy method should be called when this Middleware object is no
504552
* longer used. It removes all hooks and listeners from the Store, and

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export type WillDelRowCallback = (tableId: Id, rowId: Id) => boolean;
4747
/// WillDelValueCallback
4848
export type WillDelValueCallback = (valueId: Id) => boolean;
4949

50+
/// WillDelValuesCallback
51+
export type WillDelValuesCallback = () => boolean;
52+
5053
/// Middleware
5154
export interface Middleware {
5255
/// Middleware.getStore
@@ -73,6 +76,9 @@ export interface Middleware {
7376
/// Middleware.addWillDelValueCallback
7477
addWillDelValueCallback(callback: WillDelValueCallback): Middleware;
7578

79+
/// Middleware.addWillDelValuesCallback
80+
addWillDelValuesCallback(callback: WillDelValuesCallback): Middleware;
81+
7682
/// Middleware.destroy
7783
destroy(): void;
7884
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export type WillDelValueCallback<
102102
: never,
103103
> = (...params: Params | [valueId: never]) => boolean;
104104

105+
/// WillDelValuesCallback
106+
export type WillDelValuesCallback = () => boolean;
107+
105108
/// Middleware
106109
export interface Middleware<in out Schemas extends OptionalSchemas> {
107110
/// Middleware.getStore
@@ -142,6 +145,11 @@ export interface Middleware<in out Schemas extends OptionalSchemas> {
142145
callback: WillDelValueCallback<Schemas[1]>,
143146
): Middleware<Schemas>;
144147

148+
/// Middleware.addWillDelValuesCallback
149+
addWillDelValuesCallback(
150+
callback: WillDelValuesCallback,
151+
): Middleware<Schemas>;
152+
145153
/// Middleware.destroy
146154
destroy(): void;
147155
}

‎src/middleware/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
WillDelCellCallback,
55
WillDelRowCallback,
66
WillDelValueCallback,
7+
WillDelValuesCallback,
78
WillSetCellCallback,
89
WillSetRowCallback,
910
WillSetValueCallback,
@@ -33,6 +34,7 @@ export const createMiddleware = getCreateFunction(
3334
const willDelCellCallbacks: WillDelCellCallback[] = [];
3435
const willDelRowCallbacks: WillDelRowCallback[] = [];
3536
const willDelValueCallbacks: WillDelValueCallback[] = [];
37+
const willDelValuesCallbacks: WillDelValuesCallback[] = [];
3638

3739
const willSetCell = (
3840
tableId: Id,
@@ -86,6 +88,9 @@ export const createMiddleware = getCreateFunction(
8688
const willDelValue = (valueId: Id): boolean =>
8789
arrayEvery(willDelValueCallbacks, (callback) => callback(valueId));
8890

91+
const willDelValues = (): boolean =>
92+
arrayEvery(willDelValuesCallbacks, (callback) => callback());
93+
8994
(store as any).setInternalWillSets(
9095
willSetCell,
9196
willSetValue,
@@ -94,6 +99,7 @@ export const createMiddleware = getCreateFunction(
9499
willSetRow,
95100
willSetValues,
96101
willDelRow,
102+
willDelValues,
97103
);
98104

99105
const getStore = (): Store => store;
@@ -129,6 +135,11 @@ export const createMiddleware = getCreateFunction(
129135
callback: WillDelValueCallback,
130136
): Middleware => fluent(() => arrayPush(willDelValueCallbacks, callback));
131137

138+
const addWillDelValuesCallback = (
139+
callback: WillDelValuesCallback,
140+
): Middleware =>
141+
fluent(() => arrayPush(willDelValuesCallbacks, callback));
142+
132143
const destroy = (): void => {};
133144

134145
const middleware: Middleware = objFreeze({
@@ -140,6 +151,7 @@ export const createMiddleware = getCreateFunction(
140151
addWillDelCellCallback,
141152
addWillDelRowCallback,
142153
addWillDelValueCallback,
154+
addWillDelValuesCallback,
143155
destroy,
144156
} as Middleware);
145157

‎src/store/index.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
175175
willSetRow?: (tableId: Id, rowId: Id, row: Row) => Row | undefined,
176176
willSetValues?: (values: Values) => Values | undefined,
177177
willDelRow?: (tableId: Id, rowId: Id) => boolean,
178+
willDelValues?: () => boolean,
178179
] = [];
179180
let internalListeners: [
180181
preStartTransaction?: () => void,
@@ -1491,7 +1492,11 @@ export const createStore: typeof createStoreDecl = (): Store => {
14911492
);
14921493

14931494
const delValues = (): Store =>
1494-
fluentTransaction(() => setValidValues({}, true));
1495+
fluentTransaction(() =>
1496+
(internalWillSets[7]?.() ?? true)
1497+
? setValidValues({}, true)
1498+
: 0,
1499+
);
14951500

14961501
const delValue = (valueId: Id): Store =>
14971502
fluentTransaction(
@@ -1777,6 +1782,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
17771782
willSetRow: (tableId: Id, rowId: Id, row: Row) => Row | undefined,
17781783
willSetValues: (values: Values) => Values | undefined,
17791784
willDelRow: (tableId: Id, rowId: Id) => boolean,
1785+
willDelValues: () => boolean,
17801786
) =>
17811787
(internalWillSets = [
17821788
willSetCell,
@@ -1786,6 +1792,7 @@ export const createStore: typeof createStoreDecl = (): Store => {
17861792
willSetRow,
17871793
willSetValues,
17881794
willDelRow,
1795+
willDelValues,
17891796
]);
17901797

17911798
const setInternalListeners = (

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,112 @@ describe('willDelValue', () => {
12731273
});
12741274
});
12751275

1276+
describe('willDelValues', () => {
1277+
describe('allow', () => {
1278+
test('returning true allows delete', () => {
1279+
middleware.addWillDelValuesCallback(() => true);
1280+
store.setValues({v1: 'a', v2: 'b'});
1281+
store.delValues();
1282+
expect(store.getValues()).toEqual({});
1283+
});
1284+
});
1285+
1286+
describe('block', () => {
1287+
test('returning false blocks delete', () => {
1288+
middleware.addWillDelValuesCallback(() => false);
1289+
store.setValues({v1: 'a', v2: 'b'});
1290+
store.delValues();
1291+
expect(store.getValues()).toEqual({v1: 'a', v2: 'b'});
1292+
});
1293+
});
1294+
1295+
describe('chaining', () => {
1296+
test('all must return true to allow', () => {
1297+
middleware
1298+
.addWillDelValuesCallback(() => true)
1299+
.addWillDelValuesCallback(() => true);
1300+
store.setValues({v1: 'a'});
1301+
store.delValues();
1302+
expect(store.getValues()).toEqual({});
1303+
});
1304+
1305+
test('any returning false blocks', () => {
1306+
middleware
1307+
.addWillDelValuesCallback(() => true)
1308+
.addWillDelValuesCallback(() => false);
1309+
store.setValues({v1: 'a'});
1310+
store.delValues();
1311+
expect(store.getValues()).toEqual({v1: 'a'});
1312+
});
1313+
1314+
test('first blocks, second never called', () => {
1315+
const secondCalled = vi.fn();
1316+
middleware
1317+
.addWillDelValuesCallback(() => false)
1318+
.addWillDelValuesCallback(() => {
1319+
secondCalled();
1320+
return true;
1321+
});
1322+
store.setValues({v1: 'a'});
1323+
store.delValues();
1324+
expect(secondCalled).not.toHaveBeenCalled();
1325+
expect(store.getValues()).toEqual({v1: 'a'});
1326+
});
1327+
});
1328+
1329+
describe('entry points', () => {
1330+
test('called from delValues', () => {
1331+
const calls: number[] = [];
1332+
middleware.addWillDelValuesCallback(() => {
1333+
calls.push(1);
1334+
return true;
1335+
});
1336+
store.setValues({v1: 'a'});
1337+
store.delValues();
1338+
expect(calls).toEqual([1]);
1339+
});
1340+
1341+
test('not called from delValue', () => {
1342+
const calls: number[] = [];
1343+
middleware.addWillDelValuesCallback(() => {
1344+
calls.push(1);
1345+
return true;
1346+
});
1347+
store.setValues({v1: 'a', v2: 'b'});
1348+
store.delValue('v1');
1349+
expect(calls).toEqual([]);
1350+
expect(store.getValues()).toEqual({v2: 'b'});
1351+
});
1352+
});
1353+
1354+
describe('interaction with willDelValue', () => {
1355+
test('willDelValues blocks before willDelValue is called', () => {
1356+
const valueCalls = vi.fn();
1357+
middleware.addWillDelValuesCallback(() => false);
1358+
middleware.addWillDelValueCallback((...args) => {
1359+
valueCalls(...args);
1360+
return true;
1361+
});
1362+
store.setValues({v1: 'a', v2: 'b'});
1363+
store.delValues();
1364+
expect(valueCalls).not.toHaveBeenCalled();
1365+
expect(store.getValues()).toEqual({v1: 'a', v2: 'b'});
1366+
});
1367+
1368+
test('willDelValues allows, willDelValue still checked', () => {
1369+
const valueCalls: string[] = [];
1370+
middleware.addWillDelValuesCallback(() => true);
1371+
middleware.addWillDelValueCallback((valueId) => {
1372+
valueCalls.push(valueId);
1373+
return true;
1374+
});
1375+
store.setValues({v1: 'a', v2: 'b'});
1376+
store.delValues();
1377+
expect(valueCalls).toEqual(['v1', 'v2']);
1378+
});
1379+
});
1380+
});
1381+
12761382
describe('schema interaction', () => {
12771383
describe('willSetCell not called for schema-rejected cells', () => {
12781384
test('invalid cell type rejected before callback', () => {
@@ -1464,6 +1570,11 @@ describe('fluent API', () => {
14641570
expect(result).toBe(middleware);
14651571
});
14661572

1573+
test('addWillDelValuesCallback returns middleware', () => {
1574+
const result = middleware.addWillDelValuesCallback(() => true);
1575+
expect(result).toBe(middleware);
1576+
});
1577+
14671578
test('full chained setup', () => {
14681579
const result = middleware
14691580
.addWillSetCellCallback((_tableId, _rowId, _cellId, cell) => cell)
@@ -1472,7 +1583,8 @@ describe('fluent API', () => {
14721583
.addWillSetValuesCallback((values) => values)
14731584
.addWillDelCellCallback(() => true)
14741585
.addWillDelRowCallback(() => true)
1475-
.addWillDelValueCallback(() => true);
1586+
.addWillDelValueCallback(() => true)
1587+
.addWillDelValuesCallback(() => true);
14761588
expect(result).toBe(middleware);
14771589
});
14781590
});

‎test/unit/core/types/__snapshots__/types.test.tsx.snap‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,8 @@ exports[`Types in middleware.ts > 187:4 1`] = `"This comparison appears to be un
10941094
10951095
exports[`Types in middleware.ts > 199:17 1`] = `"Property 't2' does not exist on type 'Tables<{ readonly t0: { readonly c0: { readonly type: "number"; }; }; readonly t1: { readonly c1: { readonly type: "number"; }; readonly c1d: { readonly type: "string"; readonly default: ""; }; }; }, false>'."`;
10961096
1097+
exports[`Types in middleware.ts > 211:17 1`] = `"Property 't2' does not exist on type 'Tables<{ readonly t0: { readonly c0: { readonly type: "number"; }; }; readonly t1: { readonly c1: { readonly type: "number"; }; readonly c1d: { readonly type: "string"; readonly default: ""; }; }; }, false>'."`;
1098+
10971099
exports[`Types in persisters.ts > 24:37 1`] = `"Argument of type '[{ t1: { r1: { c2: number; }; }; }, {}]' is not assignable to parameter of type 'Content<[{ readonly t0: { readonly c0: { readonly type: "number"; }; }; readonly t1: { readonly c1: { readonly type: "number"; }; readonly c1d: { readonly type: "string"; readonly default: ""; }; }; }, { ...; }], true> | (() => Content<...>) | undefined'."`;
10981100
10991101
exports[`Types in persisters.ts > 25:25 1`] = `"Argument of type '[{ t1: { r1: { c1: string; }; }; }, {}]' is not assignable to parameter of type 'Content<[{ readonly t0: { readonly c0: { readonly type: "number"; }; }; readonly t1: { readonly c1: { readonly type: "number"; }; readonly c1d: { readonly type: "string"; readonly default: ""; }; }; }, { ...; }], true> | (() => Content<...>) | undefined'."`;

0 commit comments

Comments
 (0)