Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ interface CdeebeeSettings<T> {
modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue'
fileKey: string; // Key name for file uploads in FormData
bodyKey: string; // Key name for request body in FormData
listStrategy?: CdeebeeListStrategy<T>; // Merge strategy per list: 'merge' | 'replace'
listStrategy?: CdeebeeListStrategy<T>; // Merge strategy per list: 'merge' | 'replace' | 'skip'
mergeWithData?: unknown; // Data to merge with every request body
mergeWithHeaders?: Record<string, string>; // Headers to merge with every request
normalize?: (storage, result, strategyList) => T; // Custom normalization function
Expand Down Expand Up @@ -198,15 +198,17 @@ interface CdeebeeRequestOptions<T> {

## Data Merging Strategies

cdeebee supports two strategies for merging data:
cdeebee supports three strategies for merging data:

- **`merge`**: Merges new data with existing data, preserving existing keys not in the response
- **`replace`**: Completely replaces the list with new data
- **`skip`**: Skips updating the list entirely, preserving existing data unchanged

```typescript
listStrategy: {
forumList: 'merge', // New forums are merged with existing ones
threadList: 'replace', // Thread list is completely replaced
userList: 'skip', // User list is never updated, existing data is preserved
}
```

Expand Down
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions, type CdeebeeModule } from './reducer/types';
export { type CdeebeeListStrategy, type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions, type CdeebeeModule } from './reducer/types';
export { batchingUpdate } from './reducer/helpers';
export { request } from './reducer/request';
export { factory } from './reducer/index';
2 changes: 1 addition & 1 deletion lib/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const factory = <T>(settings: CdeebeeSettings<T>, storage?: T) => {
// Type assertion is safe here because we've already checked isRecord
const normalizedData = normalize(currentState, action.payload.result as Record<string, Record<string, unknown>>, strategyList);

// Normalize already handles merge/replace and preserves keys not in response
// Normalize already handles merge/replace/skip and preserves keys not in response
// Simply apply the result
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(state.storage as any) = normalizedData;
Expand Down
8 changes: 8 additions & 0 deletions lib/reducer/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ function applyStrategy(
return newValue as ResponseValue;
} else if (strategy === 'merge') {
return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue;
} else if (strategy === 'skip') {
return existingValue as ResponseValue;
} else {
console.warn(`Cdeebee: Unknown strategy "${strategy}" for key "${key}". Skipping normalization.`);
return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue;
Expand All @@ -63,6 +65,12 @@ export function defaultNormalize<T>(
}

const strategy = strategyList[key as keyof T] ?? 'merge';

// For 'skip' strategy, if key doesn't exist in storage, skip it entirely
if (strategy === 'skip' && !(key in currentStorage)) {
continue;
}

const existingValue = key in currentStorage ? (currentStorage[key] as StorageData) : {};

if (isDataWithPrimaryKey(responseValue)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/reducer/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type CdeebeeModule = 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue';
export type CdeebeeStrategy = 'merge' | 'replace';
export type CdeebeeStrategy = 'merge' | 'replace' | 'skip';

export type CdeebeeListStrategy<T> = Record<keyof T, CdeebeeStrategy>;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@recats/cdeebee",
"version": "3.0.0-beta.8",
"version": "3.0.0-beta.9",
"description": "React Redux data-logic library",
"repository": "git@github.com:recats/cdeebee.git",
"author": "recats",
Expand Down
86 changes: 86 additions & 0 deletions tests/lib/reducer/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,92 @@ describe('defaultNormalize', () => {
});
});

describe('skip strategy', () => {
it('should preserve existing data unchanged when skip strategy is used', () => {
mockCdeebee.storage = {
userList: {
'1': { id: '1', name: 'John', age: 30 },
'3': { id: '3', name: 'Bob' },
},
};

const response = {
userList: {
data: [
{ id: '1', name: 'John Updated', age: 31 },
{ id: '2', name: 'Jane' },
],
primaryKey: 'id',
},
} as unknown as IResponse;

const strategyList: CdeebeeListStrategy<unknown> = {
userList: 'skip',
};

const result = defaultNormalize(mockCdeebee, response, strategyList);

// Existing data should remain unchanged
expect(result.userList).toEqual({
'1': { id: '1', name: 'John', age: 30 },
'3': { id: '3', name: 'Bob' },
});
// New data from response should be ignored
expect(result.userList).not.toHaveProperty('2');
});

it('should not create new key when skip strategy is used and key does not exist', () => {
const response = {
userList: {
data: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
primaryKey: 'id',
},
} as unknown as IResponse;

const strategyList: CdeebeeListStrategy<unknown> = {
userList: 'skip',
};

const result = defaultNormalize(mockCdeebee, response, strategyList);

// Key should not be created
expect(result).not.toHaveProperty('userList');
});

it('should preserve existing data when skip strategy is used with record format', () => {
mockCdeebee.storage = {
userList: {
'1': { id: '1', name: 'John', age: 30 },
'3': { id: '3', name: 'Bob' },
},
};

const response = {
userList: {
'1': { id: '1', name: 'John Updated', age: 31 },
'2': { id: '2', name: 'Jane' },
},
} as unknown as IResponse;

const strategyList: CdeebeeListStrategy<unknown> = {
userList: 'skip',
};

const result = defaultNormalize(mockCdeebee, response, strategyList);

// Existing data should remain unchanged
expect(result.userList).toEqual({
'1': { id: '1', name: 'John', age: 30 },
'3': { id: '3', name: 'Bob' },
});
// New data from response should be ignored
expect(result.userList).not.toHaveProperty('2');
});
});

describe('unknown strategy', () => {
it('should fall back to merge strategy for unknown strategy', () => {
const response = {
Expand Down