@nexigen/entity-normalizer 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 negixenjs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Nexigen
|
|
2
|
+
|
|
3
|
+
**Entity-Normalized State Management for MobX**
|
|
4
|
+
|
|
5
|
+
Nexigen is an open-source, domain-level state management engine built on top of MobX.
|
|
6
|
+
It provides a strict **entity-first architecture** for managing normalized data,
|
|
7
|
+
relationships, collections, and async workflows in complex applications.
|
|
8
|
+
|
|
9
|
+
Nexigen is designed for applications where:
|
|
10
|
+
|
|
11
|
+
- data has long lifecycles
|
|
12
|
+
- entities are shared across multiple screens
|
|
13
|
+
- consistency and identity stability matter
|
|
14
|
+
- simple stores are no longer enough
|
|
15
|
+
|
|
16
|
+
> Nexigen brings backend-style data modeling and lifecycle guarantees to the client.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why Nexigen Exists
|
|
21
|
+
|
|
22
|
+
Most client-side state solutions treat data as:
|
|
23
|
+
|
|
24
|
+
- flat objects
|
|
25
|
+
- screen-scoped state
|
|
26
|
+
- short-lived responses
|
|
27
|
+
|
|
28
|
+
This approach breaks down when:
|
|
29
|
+
|
|
30
|
+
- the same entity appears in multiple places
|
|
31
|
+
- pagination, details, and updates overlap
|
|
32
|
+
- memory usage grows over time
|
|
33
|
+
- async logic becomes tangled with UI state
|
|
34
|
+
|
|
35
|
+
Nexigen solves this by treating **entities as first-class citizens**.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Core Goals
|
|
40
|
+
|
|
41
|
+
- **Stable Entity Identity**
|
|
42
|
+
One entity = one model instance across the entire app.
|
|
43
|
+
|
|
44
|
+
- **Normalized Graph**
|
|
45
|
+
All data lives in a single normalized entity graph.
|
|
46
|
+
|
|
47
|
+
- **Deterministic Lifecycle**
|
|
48
|
+
Entities exist only while referenced.
|
|
49
|
+
|
|
50
|
+
- **Explicit Async Commands**
|
|
51
|
+
Async logic is modeled as commands, not state.
|
|
52
|
+
|
|
53
|
+
- **MobX-native Reactivity**
|
|
54
|
+
No selectors, no memoization, no manual wiring.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Mental Model (Read First)
|
|
59
|
+
|
|
60
|
+
Before using Nexigen, you must understand its mental model.
|
|
61
|
+
|
|
62
|
+
📘 **[Mental Model](docs/docs/01-mental-model.md)**
|
|
63
|
+
Defines the rules and invariants of the system. Mandatory reading.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Architecture Overview
|
|
68
|
+
|
|
69
|
+
Nexigen is composed of several orthogonal layers:
|
|
70
|
+
|
|
71
|
+
- Entity Schemas
|
|
72
|
+
- Normalized Entity Store
|
|
73
|
+
- Records & Collections
|
|
74
|
+
- Async Ducks
|
|
75
|
+
- Dependency Injection (StoreDeps)
|
|
76
|
+
- React Hooks
|
|
77
|
+
|
|
78
|
+
📘 **[Architecture Overview](docs/docs/02-architecture-overview.md)**
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm add @nexigen/entity-normalizer mobx mobx-react-lite
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
or
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install @nexigen/entity-normalizer mobx mobx-react-lite
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
or
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
yarn add @nexigen/entity-normalizer mobx mobx-react-lite
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Application Setup
|
|
103
|
+
|
|
104
|
+
### 1. Create Entity Keys
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
export const ENTITY_KEY = {
|
|
108
|
+
VIEWER: 'viewers',
|
|
109
|
+
POST: 'posts',
|
|
110
|
+
} as const;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### 2. Define Schemas
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
export const viewerSchema = createEntitySchema(
|
|
119
|
+
ENTITY_KEY.VIEWER,
|
|
120
|
+
{},
|
|
121
|
+
{ model: ViewerModel }
|
|
122
|
+
);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
export const postSchema = createEntitySchema(
|
|
127
|
+
ENTITY_KEY.POST,
|
|
128
|
+
{
|
|
129
|
+
viewer: viewerSchema,
|
|
130
|
+
viewers: [viewerSchema],
|
|
131
|
+
},
|
|
132
|
+
{ model: PostModel }
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 3. Register Schema Map
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
export const schemaMap = {
|
|
142
|
+
[ENTITY_KEY.VIEWER]: viewerSchema,
|
|
143
|
+
[ENTITY_KEY.POST]: postSchema,
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Models
|
|
150
|
+
|
|
151
|
+
Models wrap DTOs and define behavior.
|
|
152
|
+
They never own related entities directly.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
export class PostModel {
|
|
156
|
+
constructor(dto, get) {
|
|
157
|
+
this.id = dto.id;
|
|
158
|
+
this.viewerId = dto.viewerId;
|
|
159
|
+
this.title = dto.title;
|
|
160
|
+
makeAutoObservable(this);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get viewer() {
|
|
164
|
+
return this.get(ENTITY_KEY.VIEWER, this.viewerId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Stores
|
|
172
|
+
|
|
173
|
+
Stores orchestrate behavior.
|
|
174
|
+
They never store entity data directly.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
export class PostsStore {
|
|
178
|
+
lists;
|
|
179
|
+
|
|
180
|
+
constructor(deps) {
|
|
181
|
+
this.lists = deps.core.entities.createMultiCollection({
|
|
182
|
+
entityKey: ENTITY_KEY.POST,
|
|
183
|
+
collectionId: 'posts',
|
|
184
|
+
limit: 20,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
makeAutoObservable(this);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fetchPosts = createDuck(async ({ group }) => {
|
|
191
|
+
const res = await deps.api.Posts.getPosts({ group });
|
|
192
|
+
this.lists[group].set(res);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Root Store Initialization
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
export const rootStore = createRootStore({
|
|
203
|
+
api: Api,
|
|
204
|
+
schemaMap,
|
|
205
|
+
stores: {
|
|
206
|
+
posts: PostsStore,
|
|
207
|
+
viewer: ViewerStore,
|
|
208
|
+
},
|
|
209
|
+
services: {
|
|
210
|
+
bootstrap: BootstrapService,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
registerRootStore(rootStore);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## React Integration
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
export const { useStores, useServices, useCore } =
|
|
225
|
+
createStoreHooks<typeof rootStore>();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
const PostsScreen = observer(() => {
|
|
230
|
+
const { posts } = useStores();
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
posts.fetchPosts.run({ group: 'active' });
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
return <List data={posts.lists.active.getList} />;
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Documentation Index
|
|
243
|
+
|
|
244
|
+
- EntitiesStore
|
|
245
|
+
- EntityRecord
|
|
246
|
+
- EntityCollection
|
|
247
|
+
- MultiEntityCollection
|
|
248
|
+
- Schemas
|
|
249
|
+
- Models
|
|
250
|
+
- Async Ducks
|
|
251
|
+
- StoreDeps
|
|
252
|
+
- Core API
|
|
253
|
+
- React Hooks
|
|
254
|
+
- Anti-patterns
|
|
255
|
+
- Testing
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Status
|
|
260
|
+
|
|
261
|
+
- Actively developed.
|
|
262
|
+
- Core logic is thoroughly covered by tests.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexigen/entity-normalizer",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Reactive entity infrastructure for complex client applications, built on MobX.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "eslint src",
|
|
14
|
+
"lint:fix": "eslint src --fix",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"build": "tsc"
|
|
19
|
+
},
|
|
20
|
+
"lint-staged": {
|
|
21
|
+
"*.{ts,tsx}": [
|
|
22
|
+
"eslint --fix"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"mobx": ">=6.12 <7"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/jest": "^30.0.0",
|
|
30
|
+
"@types/node": "^25.0.2",
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
|
32
|
+
"@typescript-eslint/parser": "^8.31.1",
|
|
33
|
+
"eslint": "^9.26.0",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
36
|
+
"eslint-plugin-import": "^2.31.0",
|
|
37
|
+
"husky": "^9.1.7",
|
|
38
|
+
"jest": "^30.2.0",
|
|
39
|
+
"lint-staged": "^16.2.7",
|
|
40
|
+
"mobx": "^6.13.7",
|
|
41
|
+
"prettier": "^3.7.4",
|
|
42
|
+
"ts-jest": "^29.4.6",
|
|
43
|
+
"ts-node": "^10.9.2",
|
|
44
|
+
"typescript": "^5.8.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
}
|
|
49
|
+
}
|