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
24 changes: 1 addition & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export const cdeebeeSlice = factory<Storage>(
modules: ['history', 'listener', 'cancelation', 'storage'],
fileKey: 'file',
bodyKey: 'value',
primaryKey: 'id',
listStrategy: {
forumList: 'merge',
threadList: 'replace',
Expand Down Expand Up @@ -166,7 +165,6 @@ interface CdeebeeSettings<T> {
modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation'
fileKey: string; // Key name for file uploads in FormData
bodyKey: string; // Key name for request body in FormData
primaryKey: string; // Primary key field name in API responses (default: 'primaryKey')
listStrategy?: CdeebeeListStrategy<T>; // Merge strategy per list: 'merge' | 'replace'
mergeWithData?: unknown; // Data to merge with every request body
mergeWithHeaders?: Record<string, string>; // Headers to merge with every request
Expand Down Expand Up @@ -207,27 +205,7 @@ listStrategy: {

## API Response Format

cdeebee expects API responses in a specific format for automatic normalization:

```typescript
{
forumList: {
primaryKey: 'id', // The field name specified in settings.primaryKey
data: [
{ id: 1, title: 'Forum 1' },
{ id: 2, title: 'Forum 2' },
]
},
threadList: {
primaryKey: 'id',
data: [
{ id: 101, title: 'Thread 1', forumID: 1 },
]
}
}
```

The library automatically normalizes this into:
cdeebee expects API responses in a normalized format where data is already organized as objects with keys representing entity IDs:

```typescript
{
Expand Down
23 changes: 10 additions & 13 deletions example/request/app/api/bundle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@ export async function POST(request: NextRequest) {
return NextResponse.json(
{
bundleList: {
'data': [
{
'bundleID': 961,
'timestamp': new Date().toISOString(),
'bundle': 'test123',
},
{
'bundleID': 1549103,
'timestamp': new Date().toISOString(),
'bundle': 'test4',
},
],
'primaryKey': 'bundleID',
'961': {
'bundleID': 961,
'timestamp': new Date().toISOString(),
'bundle': 'test123',
},
'1549103': {
'bundleID': 1549103,
'timestamp': new Date().toISOString(),
'bundle': 'test4',
},
},
}
);
Expand Down
1 change: 0 additions & 1 deletion example/request/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const cdeebeeSlice = factory<Storage>(
modules: ['history', 'listener', 'cancelation', 'storage'],
fileKey: 'file',
bodyKey: 'value',
primaryKey: 'primaryKey',
listStrategy: {
bundleList: 'merge',
campaignList: 'replace',
Expand Down
1 change: 0 additions & 1 deletion lib/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const initialState: CdeebeeState<unknown> = {
modules: ['history', 'listener', 'storage', 'cancelation'],
fileKey: 'file',
bodyKey: 'value',
primaryKey: 'primaryKey',
listStrategy: {},
mergeWithData: {},
mergeWithHeaders: {},
Expand Down
38 changes: 8 additions & 30 deletions lib/reducer/storage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { type CdeebeeListStrategy, type CdeebeeState } from './types';
import { hasDataProperty, hasProperty, isRecord, mergeDeepRight, omit } from './helpers';
import { isRecord, mergeDeepRight, omit } from './helpers';

type ResponseValue = Record<string, unknown> & {
data?: unknown[];
[key: string]: unknown;
};
type ResponseValue = Record<string, unknown>;

type IResponse = Record<string, ResponseValue>;

Expand All @@ -16,7 +13,6 @@ export function defaultNormalize<T>(
strategyList: CdeebeeListStrategy<T>
): Record<string, ResponseValue> {
const keyList = Object.keys(response);
const primaryKey = cdeebee.settings.primaryKey;
const currentStorage = isRecord(cdeebee.storage) ? (cdeebee.storage as Record<string, unknown>) : {};

// Start with existing storage to preserve keys not in response
Expand All @@ -31,43 +27,25 @@ export function defaultNormalize<T>(
continue;
}

if (hasDataProperty(responseValue) && hasProperty(responseValue, primaryKey)) {
const primaryKeyValue = responseValue[primaryKey];

if (typeof primaryKeyValue !== 'string') {
console.warn(`Cdeebee: Primary key "${primaryKey}" is not a string for API "${key}". Skipping normalization.`);
result[key] = responseValue;
continue;
}

// Pre-allocate storage data object
const newStorageData: StorageData = {};
const dataArray = responseValue.data;
const dataLength = dataArray.length;

for (let i = 0; i < dataLength; i++) {
const element = dataArray[i];
if (isRecord(element) && element[primaryKeyValue]) {
const elementKey = element[primaryKeyValue] as string;
newStorageData[elementKey] = element;
}
}
const isNormalized = isRecord(responseValue) && Object.keys(responseValue).length > 0;

if (isNormalized) {
const strategy = strategyList[key as keyof T] ?? 'merge';
const existingValue = key in currentStorage ? (currentStorage[key] as StorageData) : {};

if (strategy === 'replace') {
// Replace: completely replace the value
result[key] = newStorageData as ResponseValue;
result[key] = responseValue as ResponseValue;
} else if (strategy === 'merge') {
// Merge: merge with existing value
result[key] = mergeDeepRight(existingValue, newStorageData) as ResponseValue;
result[key] = mergeDeepRight(existingValue, responseValue as StorageData) as ResponseValue;
} else {
// Unknown strategy: warn and fall back to merge
console.warn(`Cdeebee: Unknown strategy "${strategy}" for key "${key}". Skipping normalization.`);
result[key] = mergeDeepRight(existingValue, newStorageData) as ResponseValue;
result[key] = mergeDeepRight(existingValue, responseValue as StorageData) as ResponseValue;
}
} else {
// Not a normalized object, store as-is
result[key] = responseValue;
}
}
Expand Down
1 change: 0 additions & 1 deletion lib/reducer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface CdeebeeSettings<T> {
modules: CdeebeeModule[];
fileKey: string;
bodyKey: string;
primaryKey: string;
mergeWithData: unknown;
mergeWithHeaders: unknown;
listStrategy?: CdeebeeListStrategy<T>;
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.3",
"version": "3.0.0-beta.4",
"description": "React Redux data-logic library",
"repository": "git@github.com:recats/cdeebee.git",
"author": "recats",
Expand Down
1 change: 0 additions & 1 deletion tests/lib/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ describe('lib/index exports', () => {
modules: [],
fileKey: 'file',
bodyKey: 'body',
primaryKey: 'id',
mergeWithData: {},
},
storage: {},
Expand Down
14 changes: 7 additions & 7 deletions tests/lib/reducer/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ describe('checkModule', () => {
modules: ['history', 'listener'] as CdeebeeModule[],
fileKey: 'file',
bodyKey: 'body',
primaryKey: 'id',
mergeWithData: {},
mergeWithHeaders: {},
listStrategy: {},
};

Expand All @@ -21,11 +21,11 @@ describe('checkModule', () => {

it('should not execute result callback when module is not included in settings', () => {
const settings: CdeebeeSettings<unknown> = {
modules: ['history', 'listener'] as CdeebeeModule[],
modules: ['history', 'listener'],
fileKey: 'file',
bodyKey: 'body',
primaryKey: 'id',
mergeWithData: {},
mergeWithHeaders: {},
listStrategy: {},
};

Expand All @@ -37,11 +37,11 @@ describe('checkModule', () => {

it('should work with all module types', () => {
const settings: CdeebeeSettings<unknown> = {
modules: ['history', 'listener', 'storage', 'cancelation'] as CdeebeeModule[],
modules: ['history', 'listener', 'storage', 'cancelation'],
fileKey: 'file',
bodyKey: 'body',
primaryKey: 'id',
mergeWithData: {},
mergeWithHeaders: {},
listStrategy: {},
};

Expand All @@ -56,11 +56,11 @@ describe('checkModule', () => {

it('should handle empty modules array', () => {
const settings: CdeebeeSettings<unknown> = {
modules: [] as CdeebeeModule[],
modules: [],
fileKey: 'file',
bodyKey: 'body',
primaryKey: 'id',
mergeWithData: {},
mergeWithHeaders: {},
listStrategy: {},
};

Expand Down
54 changes: 16 additions & 38 deletions tests/lib/reducer/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ describe('factory', () => {
modules: ['history', 'listener', 'storage', 'cancelation'],
fileKey: 'file',
bodyKey: 'value',
primaryKey: 'id',
mergeWithData: {},
mergeWithHeaders: {},
listStrategy: {},
};
});
Expand All @@ -49,8 +49,8 @@ describe('factory', () => {
modules: ['history'],
fileKey: 'customFile',
bodyKey: 'customBody',
primaryKey: 'customId',
mergeWithData: { custom: 'data' },
mergeWithHeaders: {},
listStrategy: { list: 'merge' },
};

Expand All @@ -60,7 +60,6 @@ describe('factory', () => {
const state = store.getState().cdeebee as CdeebeeState<Record<string, unknown>>;
expect(state.settings.fileKey).toBe('customFile');
expect(state.settings.bodyKey).toBe('customBody');
expect(state.settings.primaryKey).toBe('customId');
});

it('should have correct initial state structure', () => {
Expand Down Expand Up @@ -318,11 +317,8 @@ describe('factory', () => {

const mockResponse = {
userList: {
data: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
id: 'id',
'1': { id: '1', name: 'John' },
'2': { id: '2', name: 'Jane' },
},
};

Expand Down Expand Up @@ -358,11 +354,8 @@ describe('factory', () => {

const mockResponse = {
userList: {
data: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
id: 'id',
'1': { id: '1', name: 'John' },
'2': { id: '2', name: 'Jane' },
},
};

Expand Down Expand Up @@ -400,11 +393,8 @@ describe('factory', () => {
// First request - initial data
const firstResponse = {
userList: {
data: [
{ id: '1', name: 'John' },
{ id: '3', name: 'Bob' },
],
id: 'id',
'1': { id: '1', name: 'John' },
'3': { id: '3', name: 'Bob' },
},
};

Expand All @@ -420,11 +410,8 @@ describe('factory', () => {
// Second request - merge new data
const secondResponse = {
userList: {
data: [
{ id: '1', name: 'John Updated' },
{ id: '2', name: 'Jane' },
],
id: 'id',
'1': { id: '1', name: 'John Updated' },
'2': { id: '2', name: 'Jane' },
},
};

Expand Down Expand Up @@ -463,10 +450,7 @@ describe('factory', () => {

const mockResponse = {
userList: {
data: [
{ id: '1', name: 'John' },
],
id: 'id',
'1': { id: '1', name: 'John' },
},
};

Expand Down Expand Up @@ -544,8 +528,7 @@ describe('factory', () => {

const mockResponse = {
userList: {
data: [{ id: '1', name: 'John' }],
id: 'id',
'1': { id: '1', name: 'John' },
},
};

Expand Down Expand Up @@ -576,8 +559,7 @@ describe('factory', () => {
// First request for userList
const userListResponse = {
userList: {
data: [{ id: '1', name: 'John' }],
id: 'id',
'1': { id: '1', name: 'John' },
},
};

Expand All @@ -589,8 +571,7 @@ describe('factory', () => {

const postListResponse = {
postList: {
data: [{ id: '1', title: 'Post 1' }],
id: 'id',
'1': { id: '1', title: 'Post 1' },
},
};

Expand Down Expand Up @@ -670,11 +651,8 @@ describe('factory', () => {

const mockResponse = {
userList: {
data: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
id: 'id',
'1': { id: '1', name: 'John' },
'2': { id: '2', name: 'Jane' },
},
};

Expand Down
1 change: 0 additions & 1 deletion tests/lib/reducer/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ describe('request', () => {
modules: ['history', 'listener', 'cancelation'],
fileKey: 'file',
bodyKey: 'value',
primaryKey: 'id',
mergeWithData: { defaultKey: 'defaultValue' },
listStrategy: {},
};
Expand Down
Loading