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
83 changes: 81 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ For example, a forum application might have this structure:
}
```

After fetching data, the storage might look like:
After fetching data from the API (which returns data in the format `{ data: [...], primaryKey: 'id' }`), cdeebee automatically normalizes it and the storage might look like:

```typescript
{
Expand All @@ -52,6 +52,8 @@ After fetching data, the storage might look like:
}
```

**Note:** The API should return list data in the format `{ data: [...], primaryKey: 'fieldName' }`. cdeebee automatically converts this format into the normalized storage structure shown above. See the [API Response Format](#api-response-format) section for details.

### Modules

cdeebee uses a modular architecture with the following modules:
Expand Down Expand Up @@ -210,7 +212,32 @@ listStrategy: {

## API Response Format

cdeebee expects API responses in a normalized format where data is already organized as objects with keys representing entity IDs:
cdeebee expects API responses in a format where list data is provided as arrays with a `primaryKey` field. The library automatically normalizes this data into the storage structure.

### List Format

For lists (collections of entities), the API should return data in the following format:

```typescript
{
forumList: {
data: [
{ id: 1, title: 'Forum 1' },
{ id: 2, title: 'Forum 2' },
],
primaryKey: 'id',
},
threadList: {
data: [
{ id: 101, title: 'Thread 1', forumID: 1 },
{ id: 102, title: 'Thread 2', forumID: 1 },
],
primaryKey: 'id',
}
}
```

cdeebee automatically converts this format into normalized storage:

```typescript
{
Expand All @@ -220,10 +247,62 @@ cdeebee expects API responses in a normalized format where data is already organ
},
threadList: {
101: { id: 101, title: 'Thread 1', forumID: 1 },
102: { id: 102, title: 'Thread 2', forumID: 1 },
}
}
```

The `primaryKey` field specifies which property of each item should be used as the key in the normalized structure. The `primaryKey` value is converted to a string to ensure consistent key types.

**Example:**

If your API returns:
```typescript
{
sessionList: {
data: [
{
sessionID: 1,
token: 'da6ec385bc7e4f84a510c3ecca07f3',
expiresAt: '2034-03-28T22:36:09'
}
],
primaryKey: 'sessionID',
}
}
```

It will be automatically normalized to:
```typescript
{
sessionList: {
'1': {
sessionID: 1,
token: 'da6ec385bc7e4f84a510c3ecca07f3',
expiresAt: '2034-03-28T22:36:09'
}
}
}
```

### Non-List Data

For non-list data (configuration objects, simple values, etc.), you can return them as regular objects:

```typescript
{
config: {
theme: 'dark',
language: 'en',
},
userPreferences: {
notifications: true,
}
}
```

These will be stored as-is in the storage.

## Advanced Usage

### File Uploads
Expand Down
57 changes: 45 additions & 12 deletions lib/reducer/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ type IResponse = Record<string, ResponseValue>;

type StorageData = Record<string, unknown>;

function isDataWithPrimaryKey(value: unknown): value is { data: unknown[]; primaryKey: string } {
return (
isRecord(value) &&
Array.isArray(value.data) &&
typeof value.primaryKey === 'string'
);
}
function normalizeDataWithPrimaryKey(data: unknown[], primaryKey: string): Record<string, unknown> {
const result: Record<string, unknown> = {};

for (const item of data) {
if (isRecord(item) && primaryKey in item) {
const key = String(item[primaryKey]);
result[key] = item;
}
}

return result;
}

function applyStrategy(
existingValue: StorageData,
newValue: StorageData | ResponseValue,
strategy: string,
key: string
): ResponseValue {
if (strategy === 'replace') {
return newValue as ResponseValue;
} else if (strategy === 'merge') {
return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue;
} else {
console.warn(`Cdeebee: Unknown strategy "${strategy}" for key "${key}". Skipping normalization.`);
return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue;
}
}

export function defaultNormalize<T>(
cdeebee: CdeebeeState<T>,
response: IResponse,
Expand All @@ -26,20 +62,17 @@ export function defaultNormalize<T>(
continue;
}

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

if (isDataWithPrimaryKey(responseValue)) {
const normalizedValue = normalizeDataWithPrimaryKey(responseValue.data, responseValue.primaryKey);
result[key] = applyStrategy(existingValue, normalizedValue, strategy, key);
continue;
}

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

if (strategy === 'replace') {
result[key] = responseValue as ResponseValue;
} else if (strategy === 'merge') {
result[key] = mergeDeepRight(existingValue, responseValue as StorageData) as ResponseValue;
} else {
console.warn(`Cdeebee: Unknown strategy "${strategy}" for key "${key}". Skipping normalization.`);
result[key] = mergeDeepRight(existingValue, responseValue as StorageData) as ResponseValue;
}
if (isRecord(responseValue)) {
result[key] = applyStrategy(existingValue, responseValue as StorageData, strategy, key);
} else {
result[key] = responseValue;
}
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.7",
"version": "3.0.0-beta.8",
"description": "React Redux data-logic library",
"repository": "git@github.com:recats/cdeebee.git",
"author": "recats",
Expand Down
Loading