@iamjulianacosta/mobx-data 1.1.0 → 1.4.0
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/README.md +273 -102
- package/dist/{CacheHandler-BTU_rYkv.js → CacheHandler-BhfbVHed.js} +17 -20
- package/dist/CacheHandler-BhfbVHed.js.map +1 -0
- package/dist/{CacheHandler-CXgY9IJo.cjs → CacheHandler-Q5VXOgh9.cjs} +2 -2
- package/dist/CacheHandler-Q5VXOgh9.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-6mSCXsJ3.js +173 -0
- package/dist/EmbeddedRecordsMixin-6mSCXsJ3.js.map +1 -0
- package/dist/EmbeddedRecordsMixin-BkF7MdbY.cjs +2 -0
- package/dist/EmbeddedRecordsMixin-BkF7MdbY.cjs.map +1 -0
- package/dist/{JsonApiSerializer-BLoE046A.js → JsonApiSerializer-BV61cFAZ.js} +3 -3
- package/dist/JsonApiSerializer-BV61cFAZ.js.map +1 -0
- package/dist/{JsonApiSerializer-DKemcyw-.cjs → JsonApiSerializer-Dt_Y_FIo.cjs} +2 -2
- package/dist/JsonApiSerializer-Dt_Y_FIo.cjs.map +1 -0
- package/dist/JsonSerializer-BzUCyUSf.cjs +2 -0
- package/dist/JsonSerializer-BzUCyUSf.cjs.map +1 -0
- package/dist/JsonSerializer-CFqo6GjC.js +98 -0
- package/dist/JsonSerializer-CFqo6GjC.js.map +1 -0
- package/dist/MdqlMemoryExecutor-BUlsalKm.cjs +2 -0
- package/dist/MdqlMemoryExecutor-BUlsalKm.cjs.map +1 -0
- package/dist/MdqlMemoryExecutor-BWMP31zG.js +127 -0
- package/dist/MdqlMemoryExecutor-BWMP31zG.js.map +1 -0
- package/dist/{MemoryAdapter-Bp-BGHH3.js → MemoryAdapter-BW1HKixm.js} +2 -2
- package/dist/{MemoryAdapter-Bp-BGHH3.js.map → MemoryAdapter-BW1HKixm.js.map} +1 -1
- package/dist/{MemoryAdapter-DH-gzSSl.cjs → MemoryAdapter-C8iXAa2v.cjs} +2 -2
- package/dist/{MemoryAdapter-DH-gzSSl.cjs.map → MemoryAdapter-C8iXAa2v.cjs.map} +1 -1
- package/dist/{ODataAdapter-RQUjVTcf.js → ODataAdapter-CeBJblLQ.js} +25 -22
- package/dist/ODataAdapter-CeBJblLQ.js.map +1 -0
- package/dist/{ODataAdapter-CrDFvBEZ.cjs → ODataAdapter-DdE6MWkG.cjs} +2 -2
- package/dist/ODataAdapter-DdE6MWkG.cjs.map +1 -0
- package/dist/RestAdapter-D7GSrsJo.cjs +2 -0
- package/dist/RestAdapter-D7GSrsJo.cjs.map +1 -0
- package/dist/{RestAdapter-D6bGIHZT.js → RestAdapter-DYUoyV5h.js} +112 -77
- package/dist/RestAdapter-DYUoyV5h.js.map +1 -0
- package/dist/SchemaService-C_pkh-vI.js +180 -0
- package/dist/SchemaService-C_pkh-vI.js.map +1 -0
- package/dist/SchemaService-DbJLoYb9.cjs +2 -0
- package/dist/SchemaService-DbJLoYb9.cjs.map +1 -0
- package/dist/Serializer-Bap9U-kR.cjs +2 -0
- package/dist/Serializer-Bap9U-kR.cjs.map +1 -0
- package/dist/{Serializer-FxJbsZ50.js → Serializer-Ca6w_QNQ.js} +63 -49
- package/dist/Serializer-Ca6w_QNQ.js.map +1 -0
- package/dist/adapter/index.cjs +1 -1
- package/dist/adapter/index.js +2 -2
- package/dist/createStore-7PecKT54.cjs +2 -0
- package/dist/createStore-7PecKT54.cjs.map +1 -0
- package/dist/createStore-BfmRfZ_2.js +1229 -0
- package/dist/createStore-BfmRfZ_2.js.map +1 -0
- package/dist/date-Bj4O2W1F.js.map +1 -1
- package/dist/date-CRCe-9gf.cjs.map +1 -1
- package/dist/decorators-CKneHgoF.js +56 -0
- package/dist/decorators-CKneHgoF.js.map +1 -0
- package/dist/decorators-DCVYKzrL.cjs +2 -0
- package/dist/decorators-DCVYKzrL.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -90
- package/dist/index.js.map +1 -1
- package/dist/inspector/ConsoleInspector.d.ts +49 -0
- package/dist/inspector/ConsoleInspector.d.ts.map +1 -0
- package/dist/inspector/DevToolsBridge.d.ts +21 -0
- package/dist/inspector/DevToolsBridge.d.ts.map +1 -0
- package/dist/inspector/QueryParser.d.ts +21 -0
- package/dist/inspector/QueryParser.d.ts.map +1 -0
- package/dist/inspector/StoreInspector.d.ts +31 -0
- package/dist/inspector/StoreInspector.d.ts.map +1 -0
- package/dist/inspector/index.cjs +17 -0
- package/dist/inspector/index.cjs.map +1 -0
- package/dist/inspector/index.d.ts +9 -0
- package/dist/inspector/index.d.ts.map +1 -0
- package/dist/inspector/index.js +896 -0
- package/dist/inspector/index.js.map +1 -0
- package/dist/inspector/integration.d.ts +15 -0
- package/dist/inspector/integration.d.ts.map +1 -0
- package/dist/inspector/serialization.d.ts +7 -0
- package/dist/inspector/serialization.d.ts.map +1 -0
- package/dist/inspector/types.d.ts +139 -0
- package/dist/inspector/types.d.ts.map +1 -0
- package/dist/json-api/index.cjs +1 -1
- package/dist/json-api/index.js +1 -1
- package/dist/mdql/MdqlMemoryExecutor.d.ts +17 -0
- package/dist/mdql/MdqlMemoryExecutor.d.ts.map +1 -0
- package/dist/mdql/MdqlQueryBuilder.d.ts +38 -0
- package/dist/mdql/MdqlQueryBuilder.d.ts.map +1 -0
- package/dist/mdql/MdqlValidator.d.ts +13 -0
- package/dist/mdql/MdqlValidator.d.ts.map +1 -0
- package/dist/mdql/index.d.ts +6 -0
- package/dist/mdql/index.d.ts.map +1 -0
- package/dist/mdql/types.d.ts +48 -0
- package/dist/mdql/types.d.ts.map +1 -0
- package/dist/model/Model.d.ts +4 -0
- package/dist/model/Model.d.ts.map +1 -1
- package/dist/model/Snapshot.d.ts +2 -0
- package/dist/model/Snapshot.d.ts.map +1 -1
- package/dist/model/index.cjs +1 -1
- package/dist/model/index.js +1 -1
- package/dist/odata/ODataAdapter.d.ts.map +1 -1
- package/dist/odata/index.cjs +1 -1
- package/dist/odata/index.js +1 -1
- package/dist/relationships-BgM0NKdb.cjs +2 -0
- package/dist/relationships-BgM0NKdb.cjs.map +1 -0
- package/dist/{relationships-BEXANmWg.js → relationships-DvSi8fVN.js} +37 -28
- package/dist/relationships-DvSi8fVN.js.map +1 -0
- package/dist/request/CacheHandler.d.ts.map +1 -1
- package/dist/request/index.cjs +1 -1
- package/dist/request/index.js +1 -1
- package/dist/schema/SchemaService.d.ts +38 -1
- package/dist/schema/SchemaService.d.ts.map +1 -1
- package/dist/schema/decorators.d.ts +20 -1
- package/dist/schema/decorators.d.ts.map +1 -1
- package/dist/schema/index.cjs +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +10 -8
- package/dist/schema/types.d.ts +31 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/serializer/JsonSerializer.d.ts +2 -0
- package/dist/serializer/JsonSerializer.d.ts.map +1 -1
- package/dist/serializer/Serializer.d.ts +9 -0
- package/dist/serializer/Serializer.d.ts.map +1 -1
- package/dist/serializer/index.cjs +1 -1
- package/dist/serializer/index.js +6 -5
- package/dist/serializer/index.js.map +1 -1
- package/dist/store/Store.d.ts +3 -0
- package/dist/store/Store.d.ts.map +1 -1
- package/dist/store/createStore.d.ts +12 -0
- package/dist/store/createStore.d.ts.map +1 -0
- package/dist/store/index.cjs +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +5 -4
- package/dist/types-CC2fG3FP.js +8 -0
- package/dist/types-CC2fG3FP.js.map +1 -0
- package/dist/types-DCLy5XYj.cjs +2 -0
- package/dist/types-DCLy5XYj.cjs.map +1 -0
- package/package.json +7 -1
- package/src/index.ts +3 -0
- package/src/inspector/ConsoleInspector.ts +470 -0
- package/src/inspector/DevToolsBridge.ts +214 -0
- package/src/inspector/QueryParser.ts +343 -0
- package/src/inspector/StoreInspector.ts +162 -0
- package/src/inspector/index.ts +20 -0
- package/src/inspector/integration.ts +56 -0
- package/src/inspector/serialization.ts +100 -0
- package/src/inspector/types.ts +161 -0
- package/src/mdql/MdqlMemoryExecutor.ts +229 -0
- package/src/mdql/MdqlQueryBuilder.ts +170 -0
- package/src/mdql/MdqlValidator.ts +193 -0
- package/src/mdql/index.ts +21 -0
- package/src/mdql/types.ts +107 -0
- package/src/model/Model.ts +15 -0
- package/src/model/Snapshot.ts +3 -0
- package/src/odata/ODataAdapter.ts +4 -1
- package/src/request/CacheHandler.ts +2 -6
- package/src/schema/SchemaService.ts +123 -1
- package/src/schema/decorators.ts +29 -0
- package/src/schema/index.ts +1 -1
- package/src/schema/types.ts +34 -0
- package/src/serializer/JsonSerializer.ts +14 -2
- package/src/serializer/Serializer.ts +24 -1
- package/src/store/Store.ts +57 -14
- package/src/store/createStore.ts +39 -0
- package/src/store/index.ts +1 -0
- package/dist/CacheHandler-BTU_rYkv.js.map +0 -1
- package/dist/CacheHandler-CXgY9IJo.cjs.map +0 -1
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +0 -2
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +0 -1
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js +0 -261
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +0 -1
- package/dist/JsonApiSerializer-BLoE046A.js.map +0 -1
- package/dist/JsonApiSerializer-DKemcyw-.cjs.map +0 -1
- package/dist/ODataAdapter-CrDFvBEZ.cjs.map +0 -1
- package/dist/ODataAdapter-RQUjVTcf.js.map +0 -1
- package/dist/RestAdapter-CSoJg7D2.cjs +0 -2
- package/dist/RestAdapter-CSoJg7D2.cjs.map +0 -1
- package/dist/RestAdapter-D6bGIHZT.js.map +0 -1
- package/dist/SchemaService-DZwkFgZu.js +0 -102
- package/dist/SchemaService-DZwkFgZu.js.map +0 -1
- package/dist/SchemaService-Di_yjVzU.cjs +0 -2
- package/dist/SchemaService-Di_yjVzU.cjs.map +0 -1
- package/dist/Serializer-95gi5edy.cjs +0 -2
- package/dist/Serializer-95gi5edy.cjs.map +0 -1
- package/dist/Serializer-FxJbsZ50.js.map +0 -1
- package/dist/Store-Bm5JivTc.js +0 -957
- package/dist/Store-Bm5JivTc.js.map +0 -1
- package/dist/Store-DX9D0Mmy.cjs +0 -2
- package/dist/Store-DX9D0Mmy.cjs.map +0 -1
- package/dist/cache-utils-B2wFhisx.js +0 -39
- package/dist/cache-utils-B2wFhisx.js.map +0 -1
- package/dist/cache-utils-CSwsqOi3.cjs +0 -2
- package/dist/cache-utils-CSwsqOi3.cjs.map +0 -1
- package/dist/decorators-HQ1KnRdh.cjs +0 -2
- package/dist/decorators-HQ1KnRdh.cjs.map +0 -1
- package/dist/decorators-Zr35qr6A.js +0 -50
- package/dist/decorators-Zr35qr6A.js.map +0 -1
- package/dist/relationships-B55LBaCW.cjs +0 -2
- package/dist/relationships-B55LBaCW.cjs.map +0 -1
- package/dist/relationships-BEXANmWg.js.map +0 -1
- package/dist/types-C9NB2gRj.js +0 -7
- package/dist/types-C9NB2gRj.js.map +0 -1
- package/dist/types-uWOXMPWW.cjs +0 -2
- package/dist/types-uWOXMPWW.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
# mobx-data
|
|
1
|
+
# @iamjulianacosta/mobx-data
|
|
2
2
|
|
|
3
3
|
[](https://github.com/IAmJulianAcosta/mobx-data)
|
|
4
4
|
[](https://github.com/IAmJulianAcosta/mobx-data)
|
|
5
5
|
[](https://github.com/IAmJulianAcosta/mobx-data/releases)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
An Ember Data-inspired data layer for MobX applications — framework-agnostic, TypeScript-first, fully observable.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`@iamjulianacosta/mobx-data` provides a structured data layer built on MobX. It brings identity-mapped records, pluggable adapters and serializers, record state machines, and async relationships to any MobX application. All state is observable; no manual invalidation or subscriptions are needed.
|
|
15
15
|
|
|
16
16
|
### Design principles
|
|
17
17
|
|
|
18
18
|
- **Framework-agnostic** — works with React, Vue, Solid, or plain JS
|
|
19
19
|
- **MobX-native** — every piece of state is a MobX `observable`; computed props react automatically
|
|
20
20
|
- **TypeScript-first** — full generic types throughout
|
|
21
|
-
- **Ember Data
|
|
21
|
+
- **Ember Data-inspired** — familiar API patterns for Ember Data users
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -26,35 +26,43 @@ A feature-complete port of Ember Data to MobX — framework-agnostic, TypeScript
|
|
|
26
26
|
|
|
27
27
|
| Feature | Status |
|
|
28
28
|
|---------|--------|
|
|
29
|
-
| `Store` with Identity Map |
|
|
30
|
-
| `Model` base class + lifecycle hooks |
|
|
31
|
-
| `@attr`, `@belongsTo`, `@hasMany` decorators |
|
|
32
|
-
| Record state machine (`isNew`, `isDirty`, `isSaving`,
|
|
33
|
-
| Dirty tracking & `rollbackAttributes` |
|
|
34
|
-
| `RecordArray` / `AdapterPopulatedRecordArray` |
|
|
35
|
-
| `RestAdapter` |
|
|
36
|
-
| `JsonApiAdapter` |
|
|
37
|
-
| `ODataAdapter` (v4) |
|
|
38
|
-
| `JsonSerializer` / `RestSerializer` / `JsonApiSerializer` |
|
|
39
|
-
| `EmbeddedRecordsMixin` |
|
|
40
|
-
| Async & sync `belongsTo` / `hasMany` |
|
|
41
|
-
| Inverse relationship tracking |
|
|
42
|
-
|
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
29
|
+
| `Store` with Identity Map | Done |
|
|
30
|
+
| `Model` base class + lifecycle hooks | Done |
|
|
31
|
+
| `@attr`, `@belongsTo`, `@hasMany` decorators | Done |
|
|
32
|
+
| Record state machine (`isNew`, `isDirty`, `isSaving`, ...) | Done |
|
|
33
|
+
| Dirty tracking & `rollbackAttributes` | Done |
|
|
34
|
+
| `RecordArray` / `AdapterPopulatedRecordArray` | Done |
|
|
35
|
+
| `RestAdapter` | Done |
|
|
36
|
+
| `JsonApiAdapter` | Done |
|
|
37
|
+
| `ODataAdapter` (v4) | Done |
|
|
38
|
+
| `JsonSerializer` / `RestSerializer` / `JsonApiSerializer` | Done |
|
|
39
|
+
| `EmbeddedRecordsMixin` | Done |
|
|
40
|
+
| Async & sync `belongsTo` / `hasMany` | Done |
|
|
41
|
+
| Inverse relationship tracking | Done |
|
|
42
|
+
| Polymorphic models (discriminator-based) | Done |
|
|
43
|
+
| `Snapshot` | Done |
|
|
44
|
+
| `RequestManager` + handler chain | Done |
|
|
45
|
+
| `FetchHandler` / `CacheHandler` | Done |
|
|
46
|
+
| Built-in transforms (`string`, `number`, `boolean`, `date`) | Done |
|
|
47
|
+
| `SchemaService` | Done |
|
|
48
|
+
| `Errors` (field-level validation) | Done |
|
|
49
|
+
| `coalesceFindRequests` (batched findRecord calls) | Done |
|
|
50
|
+
| `IndexedDBCache` (offline-first persistent cache) | Done |
|
|
51
|
+
| MDQL query builder (`store.select()`) | Done |
|
|
48
52
|
|
|
49
53
|
---
|
|
50
54
|
|
|
51
55
|
## Installation
|
|
52
56
|
|
|
53
57
|
```bash
|
|
54
|
-
|
|
58
|
+
npm install @iamjulianacosta/mobx-data mobx reflect-metadata tsyringe
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
### Decorator compatibility
|
|
62
|
+
|
|
63
|
+
This library uses TypeScript legacy decorators and `reflect-metadata`.
|
|
64
|
+
|
|
65
|
+
Required `tsconfig.json` settings:
|
|
58
66
|
|
|
59
67
|
```json
|
|
60
68
|
{
|
|
@@ -68,9 +76,15 @@ Enable decorator metadata in your `tsconfig.json`:
|
|
|
68
76
|
Import `reflect-metadata` once at your application entry point:
|
|
69
77
|
|
|
70
78
|
```ts
|
|
71
|
-
import
|
|
79
|
+
import "reflect-metadata";
|
|
72
80
|
```
|
|
73
81
|
|
|
82
|
+
**Compatibility notes:**
|
|
83
|
+
- TypeScript 4.7+ is required
|
|
84
|
+
- Only legacy decorators (`experimentalDecorators`) are supported
|
|
85
|
+
- TC39 stage 3 decorators are not yet supported
|
|
86
|
+
- If `reflect-metadata` is missing, the library throws a clear error at startup
|
|
87
|
+
|
|
74
88
|
---
|
|
75
89
|
|
|
76
90
|
## Quick start
|
|
@@ -78,71 +92,100 @@ import 'reflect-metadata';
|
|
|
78
92
|
### 1. Define models
|
|
79
93
|
|
|
80
94
|
```ts
|
|
81
|
-
import { Model, attr, belongsTo, hasMany } from
|
|
95
|
+
import { Model, attr, belongsTo, hasMany } from "@iamjulianacosta/mobx-data/model";
|
|
82
96
|
|
|
83
97
|
class User extends Model {
|
|
84
|
-
static modelName =
|
|
98
|
+
static modelName = "user";
|
|
85
99
|
|
|
86
|
-
@attr(
|
|
87
|
-
@attr(
|
|
100
|
+
@attr("string") name!: string;
|
|
101
|
+
@attr("string") email!: string;
|
|
88
102
|
|
|
89
|
-
@hasMany(
|
|
103
|
+
@hasMany("post", { async: false, inverse: "author" })
|
|
90
104
|
posts!: ManyArray<Post>;
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
class Post extends Model {
|
|
94
|
-
static modelName =
|
|
108
|
+
static modelName = "post";
|
|
95
109
|
|
|
96
|
-
@attr(
|
|
97
|
-
@attr(
|
|
110
|
+
@attr("string") title!: string;
|
|
111
|
+
@attr("date") publishedAt!: Date | null;
|
|
98
112
|
|
|
99
|
-
@belongsTo(
|
|
113
|
+
@belongsTo("user", { async: false, inverse: "posts" })
|
|
100
114
|
author!: User | null;
|
|
101
115
|
}
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
### 2. Create a store
|
|
105
119
|
|
|
120
|
+
The simplest way to get started:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import "reflect-metadata";
|
|
124
|
+
import { createStore } from "@iamjulianacosta/mobx-data/store";
|
|
125
|
+
|
|
126
|
+
const store = createStore({
|
|
127
|
+
models: [User, Post],
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This registers all models, creates a `SchemaService`, and wires up a default `RestAdapter` and `JsonSerializer`.
|
|
132
|
+
|
|
133
|
+
For custom adapters or serializers:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createStore } from "@iamjulianacosta/mobx-data/store";
|
|
137
|
+
import { JsonApiAdapter } from "@iamjulianacosta/mobx-data/json-api";
|
|
138
|
+
import { JsonApiSerializer } from "@iamjulianacosta/mobx-data/json-api";
|
|
139
|
+
|
|
140
|
+
const store = createStore({
|
|
141
|
+
models: [User, Post],
|
|
142
|
+
adapter: new JsonApiAdapter(),
|
|
143
|
+
serializer: new JsonApiSerializer(),
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
For full control, use the manual setup:
|
|
148
|
+
|
|
106
149
|
```ts
|
|
107
|
-
import
|
|
108
|
-
import { container } from
|
|
109
|
-
import { Store } from
|
|
110
|
-
import { SchemaService } from
|
|
111
|
-
import { RestAdapter } from
|
|
112
|
-
import { JsonSerializer } from
|
|
150
|
+
import "reflect-metadata";
|
|
151
|
+
import { container } from "tsyringe";
|
|
152
|
+
import { Store } from "@iamjulianacosta/mobx-data/store";
|
|
153
|
+
import { SchemaService } from "@iamjulianacosta/mobx-data/schema";
|
|
154
|
+
import { RestAdapter } from "@iamjulianacosta/mobx-data/adapter";
|
|
155
|
+
import { JsonSerializer } from "@iamjulianacosta/mobx-data/serializer";
|
|
113
156
|
|
|
114
157
|
const schema = container.resolve(SchemaService);
|
|
115
|
-
schema.registerModel(
|
|
116
|
-
schema.registerModel(
|
|
158
|
+
schema.registerModel("user", User);
|
|
159
|
+
schema.registerModel("post", Post);
|
|
117
160
|
|
|
118
161
|
const store = container.resolve(Store);
|
|
119
162
|
|
|
120
163
|
const adapter = new RestAdapter();
|
|
121
|
-
adapter.host =
|
|
164
|
+
adapter.host = "https://api.example.com";
|
|
122
165
|
|
|
123
|
-
store.registerAdapter(
|
|
124
|
-
store.registerSerializer(
|
|
166
|
+
store.registerAdapter("application", adapter);
|
|
167
|
+
store.registerSerializer("application", new JsonSerializer());
|
|
125
168
|
```
|
|
126
169
|
|
|
127
170
|
### 3. Use the store
|
|
128
171
|
|
|
129
172
|
```ts
|
|
130
173
|
// Find a single record
|
|
131
|
-
const user = await store.findRecord(
|
|
132
|
-
console.log(user.name); //
|
|
174
|
+
const user = await store.findRecord("user", "1");
|
|
175
|
+
console.log(user.name); // "Alice"
|
|
133
176
|
|
|
134
177
|
// Find all
|
|
135
|
-
const users = await store.findAll(
|
|
178
|
+
const users = await store.findAll("user");
|
|
136
179
|
|
|
137
180
|
// Query
|
|
138
|
-
const posts = await store.query(
|
|
181
|
+
const posts = await store.query("post", { filter: { published: true } });
|
|
139
182
|
|
|
140
183
|
// Create
|
|
141
|
-
const post = store.createRecord(
|
|
184
|
+
const post = store.createRecord("post", { title: "Hello World" });
|
|
142
185
|
await post.save();
|
|
143
186
|
|
|
144
187
|
// Update
|
|
145
|
-
user.name =
|
|
188
|
+
user.name = "Bob";
|
|
146
189
|
await user.save();
|
|
147
190
|
|
|
148
191
|
// Delete
|
|
@@ -150,7 +193,7 @@ await post.destroyRecord();
|
|
|
150
193
|
|
|
151
194
|
// Push raw data
|
|
152
195
|
store.push({
|
|
153
|
-
data: { type:
|
|
196
|
+
data: { type: "user", id: "2", attributes: { name: "Carol" } },
|
|
154
197
|
});
|
|
155
198
|
```
|
|
156
199
|
|
|
@@ -163,10 +206,12 @@ store.push({
|
|
|
163
206
|
Standard REST conventions (`GET /users/1`, `POST /users`, `PUT /users/1`, `DELETE /users/1`).
|
|
164
207
|
|
|
165
208
|
```ts
|
|
209
|
+
import { RestAdapter } from "@iamjulianacosta/mobx-data/adapter";
|
|
210
|
+
|
|
166
211
|
const adapter = new RestAdapter();
|
|
167
|
-
adapter.host =
|
|
168
|
-
adapter.namespace =
|
|
169
|
-
adapter.headers = { Authorization:
|
|
212
|
+
adapter.host = "https://api.example.com";
|
|
213
|
+
adapter.namespace = "api/v2";
|
|
214
|
+
adapter.headers = { Authorization: "Bearer ..." };
|
|
170
215
|
```
|
|
171
216
|
|
|
172
217
|
### JsonApiAdapter
|
|
@@ -174,8 +219,10 @@ adapter.headers = { Authorization: 'Bearer …' };
|
|
|
174
219
|
Implements the [JSON:API](https://jsonapi.org) specification, including `application/vnd.api+json` MIME type and PATCH for updates.
|
|
175
220
|
|
|
176
221
|
```ts
|
|
222
|
+
import { JsonApiAdapter } from "@iamjulianacosta/mobx-data/json-api";
|
|
223
|
+
|
|
177
224
|
const adapter = new JsonApiAdapter();
|
|
178
|
-
adapter.host =
|
|
225
|
+
adapter.host = "https://api.example.com";
|
|
179
226
|
```
|
|
180
227
|
|
|
181
228
|
### ODataAdapter
|
|
@@ -183,16 +230,15 @@ adapter.host = 'https://api.example.com';
|
|
|
183
230
|
Implements OData v4 conventions (PascalCase entity sets, key-in-parentheses URLs, `$filter` / `$expand` / `$select` / `$top` / `$skip` / `$orderby` / `$count`).
|
|
184
231
|
|
|
185
232
|
```ts
|
|
186
|
-
import { ODataAdapter } from
|
|
233
|
+
import { ODataAdapter } from "@iamjulianacosta/mobx-data/odata";
|
|
187
234
|
|
|
188
235
|
const adapter = new ODataAdapter();
|
|
189
|
-
adapter.host =
|
|
236
|
+
adapter.host = "https://api.example.com/odata";
|
|
190
237
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
$expand: 'author,comments',
|
|
238
|
+
const posts = await adapter.query(store, "post", {
|
|
239
|
+
$expand: "author,comments",
|
|
194
240
|
$filter: "publishedAt gt '2026-01-01'",
|
|
195
|
-
$orderby:
|
|
241
|
+
$orderby: "title asc",
|
|
196
242
|
});
|
|
197
243
|
```
|
|
198
244
|
|
|
@@ -209,11 +255,11 @@ const posts = await adapter.query(store, 'post', {
|
|
|
209
255
|
### EmbeddedRecordsMixin
|
|
210
256
|
|
|
211
257
|
```ts
|
|
212
|
-
import { RestSerializer, EmbeddedRecordsMixin } from
|
|
258
|
+
import { RestSerializer, EmbeddedRecordsMixin } from "@iamjulianacosta/mobx-data/serializer";
|
|
213
259
|
|
|
214
260
|
class PostSerializer extends EmbeddedRecordsMixin(RestSerializer) {
|
|
215
261
|
attrs = {
|
|
216
|
-
comments: { embedded:
|
|
262
|
+
comments: { embedded: "always" },
|
|
217
263
|
};
|
|
218
264
|
}
|
|
219
265
|
```
|
|
@@ -224,15 +270,15 @@ class PostSerializer extends EmbeddedRecordsMixin(RestSerializer) {
|
|
|
224
270
|
|
|
225
271
|
```ts
|
|
226
272
|
// Sync (records must already be in the store)
|
|
227
|
-
@belongsTo(
|
|
228
|
-
@hasMany(
|
|
273
|
+
@belongsTo("user", { async: false }) author!: User | null;
|
|
274
|
+
@hasMany("tag", { async: false }) tags!: ManyArray<Tag>;
|
|
229
275
|
|
|
230
276
|
// Async (loaded on demand)
|
|
231
|
-
@belongsTo(
|
|
232
|
-
@hasMany(
|
|
277
|
+
@belongsTo("user", { async: true }) author!: AsyncBelongsTo<User>;
|
|
278
|
+
@hasMany("comment", { async: true }) comments!: AsyncHasMany<Comment>;
|
|
233
279
|
```
|
|
234
280
|
|
|
235
|
-
Async wrappers are `PromiseLike`
|
|
281
|
+
Async wrappers are `PromiseLike` -- they can be `await`ed or used in MobX reactions:
|
|
236
282
|
|
|
237
283
|
```ts
|
|
238
284
|
const author = await post.author; // AsyncBelongsTo<User>
|
|
@@ -242,20 +288,58 @@ console.log(post.author.isLoaded); // true
|
|
|
242
288
|
Inverse tracking is automatic when `inverse` is specified:
|
|
243
289
|
|
|
244
290
|
```ts
|
|
245
|
-
@belongsTo(
|
|
291
|
+
@belongsTo("user", { async: false, inverse: "posts" }) author!: User | null;
|
|
246
292
|
// Setting post.author automatically adds post to user.posts
|
|
247
293
|
```
|
|
248
294
|
|
|
249
295
|
---
|
|
250
296
|
|
|
297
|
+
## Polymorphic Models
|
|
298
|
+
|
|
299
|
+
Discriminator-based polymorphism lets you define abstract parent models whose concrete subtype is determined at deserialization time:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { model, attr, Model } from "@iamjulianacosta/mobx-data";
|
|
303
|
+
|
|
304
|
+
@model({
|
|
305
|
+
name: "vehicle",
|
|
306
|
+
abstract: true,
|
|
307
|
+
discriminator: {
|
|
308
|
+
key: "vehicleType", // payload field to inspect (defaults to "type")
|
|
309
|
+
map: {
|
|
310
|
+
car: () => Car,
|
|
311
|
+
motorcycle: () => Motorcycle,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
abstract class Vehicle extends Model {
|
|
316
|
+
static modelName = "vehicle";
|
|
317
|
+
@attr("string") make!: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
class Car extends Vehicle {
|
|
321
|
+
static modelName = "car";
|
|
322
|
+
@attr("number") doors!: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class Motorcycle extends Vehicle {
|
|
326
|
+
static modelName = "motorcycle";
|
|
327
|
+
@attr("number") cc!: number;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
When you push a `vehicle` record, the store reads the discriminator key and instantiates the correct concrete class. All subtypes share the parent's identity map bucket, preventing duplicates.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
251
335
|
## Transforms
|
|
252
336
|
|
|
253
337
|
```ts
|
|
254
|
-
@attr(
|
|
255
|
-
@attr(
|
|
256
|
-
@attr(
|
|
257
|
-
@attr(
|
|
258
|
-
@attr()
|
|
338
|
+
@attr("string") name!: string;
|
|
339
|
+
@attr("number") age!: number;
|
|
340
|
+
@attr("boolean") active!: boolean;
|
|
341
|
+
@attr("date") createdAt!: Date | null;
|
|
342
|
+
@attr() raw!: unknown; // pass-through
|
|
259
343
|
```
|
|
260
344
|
|
|
261
345
|
---
|
|
@@ -265,7 +349,7 @@ Inverse tracking is automatic when `inverse` is specified:
|
|
|
265
349
|
The `RequestManager` provides a middleware-style handler chain:
|
|
266
350
|
|
|
267
351
|
```ts
|
|
268
|
-
import { RequestManager, FetchHandler, CacheHandler } from
|
|
352
|
+
import { RequestManager, FetchHandler, CacheHandler } from "@iamjulianacosta/mobx-data/request";
|
|
269
353
|
|
|
270
354
|
const manager = new RequestManager()
|
|
271
355
|
.useCache(new CacheHandler())
|
|
@@ -304,31 +388,113 @@ Every record exposes observable boolean flags derived from its internal state ma
|
|
|
304
388
|
| `isValid` | No validation errors |
|
|
305
389
|
|
|
306
390
|
```ts
|
|
307
|
-
const post = store.createRecord(
|
|
308
|
-
post.isNew;
|
|
391
|
+
const post = store.createRecord("post", { title: "Draft" });
|
|
392
|
+
post.isNew; // true
|
|
309
393
|
post.isDirty; // true
|
|
310
394
|
|
|
311
395
|
await post.save();
|
|
312
|
-
post.isNew;
|
|
396
|
+
post.isNew; // false
|
|
313
397
|
post.isDirty; // false
|
|
314
398
|
```
|
|
315
399
|
|
|
316
400
|
---
|
|
317
401
|
|
|
318
|
-
##
|
|
402
|
+
## MDQL -- Query Builder
|
|
319
403
|
|
|
320
|
-
|
|
404
|
+
`store.select()` provides a chainable, type-safe query builder for filtering, sorting, and paginating records from the in-memory identity map without going through the adapter:
|
|
321
405
|
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
406
|
+
```ts
|
|
407
|
+
// Published posts, newest first, page 1
|
|
408
|
+
const posts = await store
|
|
409
|
+
.select<Post>("post")
|
|
410
|
+
.where("status", "equals", "published")
|
|
411
|
+
.orderBy("createdAt", "desc")
|
|
412
|
+
.limit(20)
|
|
413
|
+
.toArray();
|
|
414
|
+
|
|
415
|
+
// Boolean logic with nested groups
|
|
416
|
+
const users = await store
|
|
417
|
+
.select<User>("user")
|
|
418
|
+
.or((b) => {
|
|
419
|
+
b.where("name", "equals", "Alice");
|
|
420
|
+
b.where("name", "startsWith", "B");
|
|
421
|
+
})
|
|
422
|
+
.toArray();
|
|
423
|
+
|
|
424
|
+
// Reactive live query -- updates when the store changes
|
|
425
|
+
const adults = store
|
|
426
|
+
.select<User>("user")
|
|
427
|
+
.where("age", "greaterThanOrEquals", 18)
|
|
428
|
+
.toLiveArray();
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Queries compile to structured `MdqlQueryObject` data (not code), making them safe for programmatic construction and AI-generated queries. All queries are validated against the schema before execution.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## IndexedDB Cache
|
|
436
|
+
|
|
437
|
+
Register an `IndexedDBCache` to enable offline-first reads. The store checks the persistent cache before hitting the network, and automatically respects HTTP cache headers (`Cache-Control`, `Expires`):
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { IndexedDBCache } from "@iamjulianacosta/mobx-data/cache";
|
|
441
|
+
|
|
442
|
+
const cache = new IndexedDBCache({
|
|
443
|
+
databaseName: "my-app-cache", // default: "mobx-data-cache"
|
|
444
|
+
defaultTTL: 3_600_000, // default: 1 hour
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
store.registerCache(cache);
|
|
448
|
+
|
|
449
|
+
// First call: network fetch, result cached in IndexedDB
|
|
450
|
+
const user = await store.findRecord("user", "1");
|
|
451
|
+
|
|
452
|
+
// Second call (even after page reload): served from IndexedDB instantly
|
|
453
|
+
const user2 = await store.findRecord("user", "1");
|
|
326
454
|
```
|
|
327
455
|
|
|
328
|
-
|
|
456
|
+
Cache entries are automatically invalidated when records are deleted. The store parses `Cache-Control: max-age`, `s-maxage`, and `Expires` headers to set per-entry TTLs.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Coalesced Find Requests
|
|
461
|
+
|
|
462
|
+
When `adapter.coalesceFindRequests = true`, multiple concurrent `findRecord` calls for the same model type are batched into a single `findMany` network request:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
const adapter = new RestAdapter();
|
|
466
|
+
adapter.coalesceFindRequests = true;
|
|
467
|
+
store.registerAdapter("application", adapter);
|
|
468
|
+
|
|
469
|
+
// These three calls produce a single GET /users?ids[]=1&ids[]=2&ids[]=3
|
|
470
|
+
const [u1, u2, u3] = await Promise.all([
|
|
471
|
+
store.findRecord("user", "1"),
|
|
472
|
+
store.findRecord("user", "2"),
|
|
473
|
+
store.findRecord("user", "3"),
|
|
474
|
+
]);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
IDs are deduplicated. Errors propagate to all pending callers. Requests with `include` options bypass coalescing.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Testing
|
|
482
|
+
|
|
483
|
+
The project uses [Vitest](https://vitest.dev) with 900+ tests covering every subsystem:
|
|
484
|
+
|
|
485
|
+
- **Unit tests** — model lifecycle, state machine, dirty tracking, snapshots, errors, transforms
|
|
486
|
+
- **Adapter tests** — RestAdapter, JsonApiAdapter, ODataAdapter, MemoryAdapter, buildURL
|
|
487
|
+
- **Serializer tests** — JsonSerializer, RestSerializer, JsonApiSerializer, EmbeddedRecordsMixin
|
|
488
|
+
- **Store tests** — identity map, push, peek, find, query, save, relationships, polymorphism, live queries
|
|
489
|
+
- **MDQL tests** — query builder, validator, memory executor, store integration
|
|
490
|
+
- **Cache tests** — IndexedDB cache, cache utilities, store integration
|
|
491
|
+
- **E2E tests** — full round-trip tests against in-process OData and JSON:API servers
|
|
329
492
|
|
|
330
493
|
```bash
|
|
331
|
-
|
|
494
|
+
npm run test # run all tests once
|
|
495
|
+
npm run test:watch # watch mode
|
|
496
|
+
npm run test:coverage # run with coverage report
|
|
497
|
+
npm run typecheck # TypeScript type check
|
|
332
498
|
```
|
|
333
499
|
|
|
334
500
|
---
|
|
@@ -337,21 +503,26 @@ bun tests/e2e/fixtures/run-bun.ts
|
|
|
337
503
|
|
|
338
504
|
```
|
|
339
505
|
src/
|
|
340
|
-
adapter/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
506
|
+
adapter/ -- Adapter, RestAdapter, MemoryAdapter
|
|
507
|
+
cache/ -- IndexedDBCache, CacheLike interface, cache-utils
|
|
508
|
+
json-api/ -- JsonApiAdapter, JsonApiSerializer
|
|
509
|
+
mdql/ -- MdqlQueryBuilder, MdqlValidator, MdqlMemoryExecutor
|
|
510
|
+
model/ -- Model, StateMachine, Errors, Snapshot, relationships
|
|
511
|
+
odata/ -- ODataAdapter
|
|
512
|
+
request/ -- RequestManager, FetchHandler, CacheHandler
|
|
513
|
+
schema/ -- SchemaService, decorators (@attr, @belongsTo, @hasMany, @model)
|
|
514
|
+
serializer/ -- Serializer, JsonSerializer, RestSerializer, EmbeddedRecordsMixin
|
|
515
|
+
store/ -- Store, IdentityMap, RecordArray, createStore
|
|
516
|
+
transforms/ -- Transform, BooleanTransform, DateTransform, NumberTransform, StringTransform
|
|
349
517
|
tests/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
518
|
+
adapter/ -- adapter unit tests
|
|
519
|
+
cache/ -- IndexedDB cache and store integration tests
|
|
520
|
+
coverage/ -- edge-case tests
|
|
521
|
+
e2e/ -- end-to-end tests (OData server, JSON:API, cache)
|
|
522
|
+
mdql/ -- MDQL query builder, validator, executor tests
|
|
523
|
+
model/ -- model unit tests
|
|
524
|
+
store/ -- store operation tests
|
|
525
|
+
(...)
|
|
355
526
|
```
|
|
356
527
|
|
|
357
528
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { injectable as u } from "tsyringe";
|
|
2
|
-
var p = Object.getOwnPropertyDescriptor,
|
|
2
|
+
var p = Object.getOwnPropertyDescriptor, y = (e, r, t, s) => {
|
|
3
3
|
for (var n = s > 1 ? void 0 : s ? p(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
4
4
|
(c = e[a]) && (n = c(n) || n);
|
|
5
5
|
return n;
|
|
@@ -59,17 +59,17 @@ let d = class {
|
|
|
59
59
|
setResponse(l) {
|
|
60
60
|
this.response = l;
|
|
61
61
|
}
|
|
62
|
-
},
|
|
63
|
-
return a.request(c,
|
|
62
|
+
}, h = (l) => s(l);
|
|
63
|
+
return a.request(c, h);
|
|
64
64
|
};
|
|
65
65
|
return s(e);
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
-
d =
|
|
68
|
+
d = y([
|
|
69
69
|
u()
|
|
70
70
|
], d);
|
|
71
|
-
var
|
|
72
|
-
for (var n = s > 1 ? void 0 : s ?
|
|
71
|
+
var f = Object.getOwnPropertyDescriptor, _ = (e, r, t, s) => {
|
|
72
|
+
for (var n = s > 1 ? void 0 : s ? f(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
73
73
|
(c = e[a]) && (n = c(n) || n);
|
|
74
74
|
return n;
|
|
75
75
|
};
|
|
@@ -78,7 +78,7 @@ class v extends Error {
|
|
|
78
78
|
super(n ?? `Request failed with status ${r}`), this.status = r, this.content = t, this.headers = s;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
let
|
|
81
|
+
let i = class {
|
|
82
82
|
static headersToObject(e) {
|
|
83
83
|
const r = {};
|
|
84
84
|
return e.forEach((t, s) => {
|
|
@@ -113,7 +113,7 @@ let h = class {
|
|
|
113
113
|
headers: t.headers
|
|
114
114
|
};
|
|
115
115
|
t.body !== void 0 && t.body !== null && (s.body = t.body), t.signal && (s.signal = t.signal);
|
|
116
|
-
const n = await fetch(t.url, s), a = await
|
|
116
|
+
const n = await fetch(t.url, s), a = await i.parseBody(n), c = i.headersToObject(n.headers);
|
|
117
117
|
if (!n.ok)
|
|
118
118
|
throw new v(n.status, a, c);
|
|
119
119
|
return {
|
|
@@ -124,9 +124,9 @@ let h = class {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
|
-
|
|
127
|
+
i = _([
|
|
128
128
|
u()
|
|
129
|
-
],
|
|
129
|
+
], i);
|
|
130
130
|
var w = Object.getOwnPropertyDescriptor, g = (e, r, t, s) => {
|
|
131
131
|
for (var n = s > 1 ? void 0 : s ? w(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
132
132
|
(c = e[a]) && (n = c(n) || n);
|
|
@@ -142,10 +142,7 @@ let o = class {
|
|
|
142
142
|
evictLRU() {
|
|
143
143
|
for (; this.cache.size > this.maxSize; ) {
|
|
144
144
|
const e = this.cache.keys().next().value;
|
|
145
|
-
|
|
146
|
-
this.cache.delete(e);
|
|
147
|
-
else
|
|
148
|
-
break;
|
|
145
|
+
this.cache.delete(e);
|
|
149
146
|
}
|
|
150
147
|
}
|
|
151
148
|
static keyFor(e) {
|
|
@@ -171,12 +168,12 @@ let o = class {
|
|
|
171
168
|
return r(t);
|
|
172
169
|
const s = o.keyFor(t);
|
|
173
170
|
if (!(((c = t.cacheOptions) == null ? void 0 : c.reload) === !0)) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
if (this.isExpired(
|
|
171
|
+
const h = this.cache.get(s);
|
|
172
|
+
if (h)
|
|
173
|
+
if (this.isExpired(h))
|
|
177
174
|
this.cache.delete(s);
|
|
178
175
|
else
|
|
179
|
-
return this.cache.delete(s), this.cache.set(s,
|
|
176
|
+
return this.cache.delete(s), this.cache.set(s, h), h.response;
|
|
180
177
|
}
|
|
181
178
|
const a = await r(t);
|
|
182
179
|
return this.cache.set(s, { response: a, cachedAt: Date.now() }), this.evictLRU(), a;
|
|
@@ -202,7 +199,7 @@ o = g([
|
|
|
202
199
|
], o);
|
|
203
200
|
export {
|
|
204
201
|
o as C,
|
|
205
|
-
|
|
202
|
+
i as F,
|
|
206
203
|
d as R
|
|
207
204
|
};
|
|
208
|
-
//# sourceMappingURL=CacheHandler-
|
|
205
|
+
//# sourceMappingURL=CacheHandler-BhfbVHed.js.map
|