@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.
Files changed (203) hide show
  1. package/README.md +273 -102
  2. package/dist/{CacheHandler-BTU_rYkv.js → CacheHandler-BhfbVHed.js} +17 -20
  3. package/dist/CacheHandler-BhfbVHed.js.map +1 -0
  4. package/dist/{CacheHandler-CXgY9IJo.cjs → CacheHandler-Q5VXOgh9.cjs} +2 -2
  5. package/dist/CacheHandler-Q5VXOgh9.cjs.map +1 -0
  6. package/dist/EmbeddedRecordsMixin-6mSCXsJ3.js +173 -0
  7. package/dist/EmbeddedRecordsMixin-6mSCXsJ3.js.map +1 -0
  8. package/dist/EmbeddedRecordsMixin-BkF7MdbY.cjs +2 -0
  9. package/dist/EmbeddedRecordsMixin-BkF7MdbY.cjs.map +1 -0
  10. package/dist/{JsonApiSerializer-BLoE046A.js → JsonApiSerializer-BV61cFAZ.js} +3 -3
  11. package/dist/JsonApiSerializer-BV61cFAZ.js.map +1 -0
  12. package/dist/{JsonApiSerializer-DKemcyw-.cjs → JsonApiSerializer-Dt_Y_FIo.cjs} +2 -2
  13. package/dist/JsonApiSerializer-Dt_Y_FIo.cjs.map +1 -0
  14. package/dist/JsonSerializer-BzUCyUSf.cjs +2 -0
  15. package/dist/JsonSerializer-BzUCyUSf.cjs.map +1 -0
  16. package/dist/JsonSerializer-CFqo6GjC.js +98 -0
  17. package/dist/JsonSerializer-CFqo6GjC.js.map +1 -0
  18. package/dist/MdqlMemoryExecutor-BUlsalKm.cjs +2 -0
  19. package/dist/MdqlMemoryExecutor-BUlsalKm.cjs.map +1 -0
  20. package/dist/MdqlMemoryExecutor-BWMP31zG.js +127 -0
  21. package/dist/MdqlMemoryExecutor-BWMP31zG.js.map +1 -0
  22. package/dist/{MemoryAdapter-Bp-BGHH3.js → MemoryAdapter-BW1HKixm.js} +2 -2
  23. package/dist/{MemoryAdapter-Bp-BGHH3.js.map → MemoryAdapter-BW1HKixm.js.map} +1 -1
  24. package/dist/{MemoryAdapter-DH-gzSSl.cjs → MemoryAdapter-C8iXAa2v.cjs} +2 -2
  25. package/dist/{MemoryAdapter-DH-gzSSl.cjs.map → MemoryAdapter-C8iXAa2v.cjs.map} +1 -1
  26. package/dist/{ODataAdapter-RQUjVTcf.js → ODataAdapter-CeBJblLQ.js} +25 -22
  27. package/dist/ODataAdapter-CeBJblLQ.js.map +1 -0
  28. package/dist/{ODataAdapter-CrDFvBEZ.cjs → ODataAdapter-DdE6MWkG.cjs} +2 -2
  29. package/dist/ODataAdapter-DdE6MWkG.cjs.map +1 -0
  30. package/dist/RestAdapter-D7GSrsJo.cjs +2 -0
  31. package/dist/RestAdapter-D7GSrsJo.cjs.map +1 -0
  32. package/dist/{RestAdapter-D6bGIHZT.js → RestAdapter-DYUoyV5h.js} +112 -77
  33. package/dist/RestAdapter-DYUoyV5h.js.map +1 -0
  34. package/dist/SchemaService-C_pkh-vI.js +180 -0
  35. package/dist/SchemaService-C_pkh-vI.js.map +1 -0
  36. package/dist/SchemaService-DbJLoYb9.cjs +2 -0
  37. package/dist/SchemaService-DbJLoYb9.cjs.map +1 -0
  38. package/dist/Serializer-Bap9U-kR.cjs +2 -0
  39. package/dist/Serializer-Bap9U-kR.cjs.map +1 -0
  40. package/dist/{Serializer-FxJbsZ50.js → Serializer-Ca6w_QNQ.js} +63 -49
  41. package/dist/Serializer-Ca6w_QNQ.js.map +1 -0
  42. package/dist/adapter/index.cjs +1 -1
  43. package/dist/adapter/index.js +2 -2
  44. package/dist/createStore-7PecKT54.cjs +2 -0
  45. package/dist/createStore-7PecKT54.cjs.map +1 -0
  46. package/dist/createStore-BfmRfZ_2.js +1229 -0
  47. package/dist/createStore-BfmRfZ_2.js.map +1 -0
  48. package/dist/date-Bj4O2W1F.js.map +1 -1
  49. package/dist/date-CRCe-9gf.cjs.map +1 -1
  50. package/dist/decorators-CKneHgoF.js +56 -0
  51. package/dist/decorators-CKneHgoF.js.map +1 -0
  52. package/dist/decorators-DCVYKzrL.cjs +2 -0
  53. package/dist/decorators-DCVYKzrL.cjs.map +1 -0
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.ts +2 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +100 -90
  59. package/dist/index.js.map +1 -1
  60. package/dist/inspector/ConsoleInspector.d.ts +49 -0
  61. package/dist/inspector/ConsoleInspector.d.ts.map +1 -0
  62. package/dist/inspector/DevToolsBridge.d.ts +21 -0
  63. package/dist/inspector/DevToolsBridge.d.ts.map +1 -0
  64. package/dist/inspector/QueryParser.d.ts +21 -0
  65. package/dist/inspector/QueryParser.d.ts.map +1 -0
  66. package/dist/inspector/StoreInspector.d.ts +31 -0
  67. package/dist/inspector/StoreInspector.d.ts.map +1 -0
  68. package/dist/inspector/index.cjs +17 -0
  69. package/dist/inspector/index.cjs.map +1 -0
  70. package/dist/inspector/index.d.ts +9 -0
  71. package/dist/inspector/index.d.ts.map +1 -0
  72. package/dist/inspector/index.js +896 -0
  73. package/dist/inspector/index.js.map +1 -0
  74. package/dist/inspector/integration.d.ts +15 -0
  75. package/dist/inspector/integration.d.ts.map +1 -0
  76. package/dist/inspector/serialization.d.ts +7 -0
  77. package/dist/inspector/serialization.d.ts.map +1 -0
  78. package/dist/inspector/types.d.ts +139 -0
  79. package/dist/inspector/types.d.ts.map +1 -0
  80. package/dist/json-api/index.cjs +1 -1
  81. package/dist/json-api/index.js +1 -1
  82. package/dist/mdql/MdqlMemoryExecutor.d.ts +17 -0
  83. package/dist/mdql/MdqlMemoryExecutor.d.ts.map +1 -0
  84. package/dist/mdql/MdqlQueryBuilder.d.ts +38 -0
  85. package/dist/mdql/MdqlQueryBuilder.d.ts.map +1 -0
  86. package/dist/mdql/MdqlValidator.d.ts +13 -0
  87. package/dist/mdql/MdqlValidator.d.ts.map +1 -0
  88. package/dist/mdql/index.d.ts +6 -0
  89. package/dist/mdql/index.d.ts.map +1 -0
  90. package/dist/mdql/types.d.ts +48 -0
  91. package/dist/mdql/types.d.ts.map +1 -0
  92. package/dist/model/Model.d.ts +4 -0
  93. package/dist/model/Model.d.ts.map +1 -1
  94. package/dist/model/Snapshot.d.ts +2 -0
  95. package/dist/model/Snapshot.d.ts.map +1 -1
  96. package/dist/model/index.cjs +1 -1
  97. package/dist/model/index.js +1 -1
  98. package/dist/odata/ODataAdapter.d.ts.map +1 -1
  99. package/dist/odata/index.cjs +1 -1
  100. package/dist/odata/index.js +1 -1
  101. package/dist/relationships-BgM0NKdb.cjs +2 -0
  102. package/dist/relationships-BgM0NKdb.cjs.map +1 -0
  103. package/dist/{relationships-BEXANmWg.js → relationships-DvSi8fVN.js} +37 -28
  104. package/dist/relationships-DvSi8fVN.js.map +1 -0
  105. package/dist/request/CacheHandler.d.ts.map +1 -1
  106. package/dist/request/index.cjs +1 -1
  107. package/dist/request/index.js +1 -1
  108. package/dist/schema/SchemaService.d.ts +38 -1
  109. package/dist/schema/SchemaService.d.ts.map +1 -1
  110. package/dist/schema/decorators.d.ts +20 -1
  111. package/dist/schema/decorators.d.ts.map +1 -1
  112. package/dist/schema/index.cjs +1 -1
  113. package/dist/schema/index.d.ts +1 -1
  114. package/dist/schema/index.d.ts.map +1 -1
  115. package/dist/schema/index.js +10 -8
  116. package/dist/schema/types.d.ts +31 -0
  117. package/dist/schema/types.d.ts.map +1 -1
  118. package/dist/serializer/JsonSerializer.d.ts +2 -0
  119. package/dist/serializer/JsonSerializer.d.ts.map +1 -1
  120. package/dist/serializer/Serializer.d.ts +9 -0
  121. package/dist/serializer/Serializer.d.ts.map +1 -1
  122. package/dist/serializer/index.cjs +1 -1
  123. package/dist/serializer/index.js +6 -5
  124. package/dist/serializer/index.js.map +1 -1
  125. package/dist/store/Store.d.ts +3 -0
  126. package/dist/store/Store.d.ts.map +1 -1
  127. package/dist/store/createStore.d.ts +12 -0
  128. package/dist/store/createStore.d.ts.map +1 -0
  129. package/dist/store/index.cjs +1 -1
  130. package/dist/store/index.d.ts +1 -0
  131. package/dist/store/index.d.ts.map +1 -1
  132. package/dist/store/index.js +5 -4
  133. package/dist/types-CC2fG3FP.js +8 -0
  134. package/dist/types-CC2fG3FP.js.map +1 -0
  135. package/dist/types-DCLy5XYj.cjs +2 -0
  136. package/dist/types-DCLy5XYj.cjs.map +1 -0
  137. package/package.json +7 -1
  138. package/src/index.ts +3 -0
  139. package/src/inspector/ConsoleInspector.ts +470 -0
  140. package/src/inspector/DevToolsBridge.ts +214 -0
  141. package/src/inspector/QueryParser.ts +343 -0
  142. package/src/inspector/StoreInspector.ts +162 -0
  143. package/src/inspector/index.ts +20 -0
  144. package/src/inspector/integration.ts +56 -0
  145. package/src/inspector/serialization.ts +100 -0
  146. package/src/inspector/types.ts +161 -0
  147. package/src/mdql/MdqlMemoryExecutor.ts +229 -0
  148. package/src/mdql/MdqlQueryBuilder.ts +170 -0
  149. package/src/mdql/MdqlValidator.ts +193 -0
  150. package/src/mdql/index.ts +21 -0
  151. package/src/mdql/types.ts +107 -0
  152. package/src/model/Model.ts +15 -0
  153. package/src/model/Snapshot.ts +3 -0
  154. package/src/odata/ODataAdapter.ts +4 -1
  155. package/src/request/CacheHandler.ts +2 -6
  156. package/src/schema/SchemaService.ts +123 -1
  157. package/src/schema/decorators.ts +29 -0
  158. package/src/schema/index.ts +1 -1
  159. package/src/schema/types.ts +34 -0
  160. package/src/serializer/JsonSerializer.ts +14 -2
  161. package/src/serializer/Serializer.ts +24 -1
  162. package/src/store/Store.ts +57 -14
  163. package/src/store/createStore.ts +39 -0
  164. package/src/store/index.ts +1 -0
  165. package/dist/CacheHandler-BTU_rYkv.js.map +0 -1
  166. package/dist/CacheHandler-CXgY9IJo.cjs.map +0 -1
  167. package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +0 -2
  168. package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +0 -1
  169. package/dist/EmbeddedRecordsMixin-VoHluHCT.js +0 -261
  170. package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +0 -1
  171. package/dist/JsonApiSerializer-BLoE046A.js.map +0 -1
  172. package/dist/JsonApiSerializer-DKemcyw-.cjs.map +0 -1
  173. package/dist/ODataAdapter-CrDFvBEZ.cjs.map +0 -1
  174. package/dist/ODataAdapter-RQUjVTcf.js.map +0 -1
  175. package/dist/RestAdapter-CSoJg7D2.cjs +0 -2
  176. package/dist/RestAdapter-CSoJg7D2.cjs.map +0 -1
  177. package/dist/RestAdapter-D6bGIHZT.js.map +0 -1
  178. package/dist/SchemaService-DZwkFgZu.js +0 -102
  179. package/dist/SchemaService-DZwkFgZu.js.map +0 -1
  180. package/dist/SchemaService-Di_yjVzU.cjs +0 -2
  181. package/dist/SchemaService-Di_yjVzU.cjs.map +0 -1
  182. package/dist/Serializer-95gi5edy.cjs +0 -2
  183. package/dist/Serializer-95gi5edy.cjs.map +0 -1
  184. package/dist/Serializer-FxJbsZ50.js.map +0 -1
  185. package/dist/Store-Bm5JivTc.js +0 -957
  186. package/dist/Store-Bm5JivTc.js.map +0 -1
  187. package/dist/Store-DX9D0Mmy.cjs +0 -2
  188. package/dist/Store-DX9D0Mmy.cjs.map +0 -1
  189. package/dist/cache-utils-B2wFhisx.js +0 -39
  190. package/dist/cache-utils-B2wFhisx.js.map +0 -1
  191. package/dist/cache-utils-CSwsqOi3.cjs +0 -2
  192. package/dist/cache-utils-CSwsqOi3.cjs.map +0 -1
  193. package/dist/decorators-HQ1KnRdh.cjs +0 -2
  194. package/dist/decorators-HQ1KnRdh.cjs.map +0 -1
  195. package/dist/decorators-Zr35qr6A.js +0 -50
  196. package/dist/decorators-Zr35qr6A.js.map +0 -1
  197. package/dist/relationships-B55LBaCW.cjs +0 -2
  198. package/dist/relationships-B55LBaCW.cjs.map +0 -1
  199. package/dist/relationships-BEXANmWg.js.map +0 -1
  200. package/dist/types-C9NB2gRj.js +0 -7
  201. package/dist/types-C9NB2gRj.js.map +0 -1
  202. package/dist/types-uWOXMPWW.cjs +0 -2
  203. 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
  [![build](https://img.shields.io/endpoint?url=https://iamjulianacosta.github.io/mobx-data/build-badge.json)](https://github.com/IAmJulianAcosta/mobx-data)
4
4
  [![coverage](https://img.shields.io/endpoint?url=https://iamjulianacosta.github.io/mobx-data/coverage-badge.json)](https://github.com/IAmJulianAcosta/mobx-data)
5
5
  [![version](https://img.shields.io/endpoint?url=https://iamjulianacosta.github.io/mobx-data/version-badge.json)](https://github.com/IAmJulianAcosta/mobx-data/releases)
6
6
  [![license](https://img.shields.io/endpoint?url=https://iamjulianacosta.github.io/mobx-data/license-badge.json)](./LICENSE)
7
7
 
8
- A feature-complete port of Ember Data to MobX — framework-agnostic, TypeScript-first, fully observable.
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
- `mobx-data` brings the battle-tested Ember Data mental model (Identity Map, pluggable adapters/serializers, record state machines, async relationships) to any MobX application. All state is observable; no manual invalidation or subscriptions are needed.
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 parity** — near 1-to-1 API mapping so Ember Data users feel at home
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
- | `Snapshot` | |
43
- | `RequestManager` + handler chain | |
44
- | `FetchHandler` / `CacheHandler` | |
45
- | Built-in transforms (`string`, `number`, `boolean`, `date`) | |
46
- | `SchemaService` | |
47
- | `Errors` (field-level validation) | |
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
- pnpm add mobx-data mobx reflect-metadata tsyringe
58
+ npm install @iamjulianacosta/mobx-data mobx reflect-metadata tsyringe
55
59
  ```
56
60
 
57
- Enable decorator metadata in your `tsconfig.json`:
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 'reflect-metadata';
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 'mobx-data/model';
95
+ import { Model, attr, belongsTo, hasMany } from "@iamjulianacosta/mobx-data/model";
82
96
 
83
97
  class User extends Model {
84
- static modelName = 'user';
98
+ static modelName = "user";
85
99
 
86
- @attr('string') name!: string;
87
- @attr('string') email!: string;
100
+ @attr("string") name!: string;
101
+ @attr("string") email!: string;
88
102
 
89
- @hasMany('post', { async: false, inverse: 'author' })
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 = 'post';
108
+ static modelName = "post";
95
109
 
96
- @attr('string') title!: string;
97
- @attr('date') publishedAt!: Date | null;
110
+ @attr("string") title!: string;
111
+ @attr("date") publishedAt!: Date | null;
98
112
 
99
- @belongsTo('user', { async: false, inverse: 'posts' })
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 'reflect-metadata';
108
- import { container } from 'tsyringe';
109
- import { Store } from 'mobx-data/store';
110
- import { SchemaService } from 'mobx-data/schema';
111
- import { RestAdapter } from 'mobx-data/adapter';
112
- import { JsonSerializer } from 'mobx-data/serializer';
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('user', User);
116
- schema.registerModel('post', Post);
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 = 'https://api.example.com';
164
+ adapter.host = "https://api.example.com";
122
165
 
123
- store.registerAdapter('application', adapter);
124
- store.registerSerializer('application', new JsonSerializer());
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('user', '1');
132
- console.log(user.name); // 'Alice'
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('user');
178
+ const users = await store.findAll("user");
136
179
 
137
180
  // Query
138
- const posts = await store.query('post', { filter: { published: true } });
181
+ const posts = await store.query("post", { filter: { published: true } });
139
182
 
140
183
  // Create
141
- const post = store.createRecord('post', { title: 'Hello World' });
184
+ const post = store.createRecord("post", { title: "Hello World" });
142
185
  await post.save();
143
186
 
144
187
  // Update
145
- user.name = 'Bob';
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: 'user', id: '2', attributes: { name: 'Carol' } }
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 = 'https://api.example.com';
168
- adapter.namespace = 'api/v2';
169
- adapter.headers = { Authorization: 'Bearer …' };
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 = 'https://api.example.com';
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 'mobx-data/odata';
233
+ import { ODataAdapter } from "@iamjulianacosta/mobx-data/odata";
187
234
 
188
235
  const adapter = new ODataAdapter();
189
- adapter.host = 'https://api.example.com/odata';
236
+ adapter.host = "https://api.example.com/odata";
190
237
 
191
- // $expand navigation properties
192
- const posts = await adapter.query(store, 'post', {
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: 'title asc',
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 'mobx-data/serializer';
258
+ import { RestSerializer, EmbeddedRecordsMixin } from "@iamjulianacosta/mobx-data/serializer";
213
259
 
214
260
  class PostSerializer extends EmbeddedRecordsMixin(RestSerializer) {
215
261
  attrs = {
216
- comments: { embedded: 'always' },
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('user', { async: false }) author!: User | null;
228
- @hasMany('tag', { async: false }) tags!: ManyArray<Tag>;
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('user', { async: true }) author!: AsyncBelongsTo<User>;
232
- @hasMany('comment', { async: true }) comments!: AsyncHasMany<Comment>;
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` they can be `await`ed or used in MobX reactions:
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('user', { async: false, inverse: 'posts' }) author!: User | null;
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('string') name!: string;
255
- @attr('number') age!: number;
256
- @attr('boolean') active!: boolean;
257
- @attr('date') createdAt!: Date | null;
258
- @attr() raw!: unknown; // pass-through
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 'mobx-data/request';
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('post', { title: 'Draft' });
308
- post.isNew; // true
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; // false
396
+ post.isNew; // false
313
397
  post.isDirty; // false
314
398
  ```
315
399
 
316
400
  ---
317
401
 
318
- ## Testing
402
+ ## MDQL -- Query Builder
319
403
 
320
- The project includes a full end-to-end test suite against an in-process OData server built with [`simple-odata-server`](https://github.com/pofider/simple-odata-server).
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
- ```bash
323
- pnpm test # run all tests once
324
- pnpm test:watch # watch mode
325
- pnpm typecheck # TypeScript type check
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
- To run the test OData server manually:
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
- bun tests/e2e/fixtures/run-bun.ts
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/ Adapter, RestAdapter
341
- json-api/ JsonApiAdapter, JsonApiSerializer
342
- model/ Model, StateMachine, Errors, Snapshot, relationships
343
- odata/ ODataAdapter
344
- request/ RequestManager, FetchHandler, CacheHandler, types
345
- schema/ SchemaService, decorators (@attr, @belongsTo, @hasMany), types
346
- serializer/ Serializer, JsonSerializer, RestSerializer, EmbeddedRecordsMixin
347
- store/ Store, IdentityMap, RecordArray
348
- transforms/ Transform, BooleanTransform, DateTransform, NumberTransform, StringTransform
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
- e2e/ end-to-end tests against a live OData server
351
- fixtures/ in-process OData server, seed data, filter engine
352
- adapter/ adapter unit tests
353
- model/ model unit tests
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, f = (e, r, t, s) => {
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
- }, i = (l) => s(l);
63
- return a.request(c, i);
62
+ }, h = (l) => s(l);
63
+ return a.request(c, h);
64
64
  };
65
65
  return s(e);
66
66
  }
67
67
  };
68
- d = f([
68
+ d = y([
69
69
  u()
70
70
  ], d);
71
- var y = Object.getOwnPropertyDescriptor, _ = (e, r, t, s) => {
72
- for (var n = s > 1 ? void 0 : s ? y(r, t) : r, a = e.length - 1, c; a >= 0; a--)
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 h = class {
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 h.parseBody(n), c = h.headersToObject(n.headers);
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
- h = _([
127
+ i = _([
128
128
  u()
129
- ], h);
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
- if (e !== void 0)
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 i = this.cache.get(s);
175
- if (i)
176
- if (this.isExpired(i))
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, i), i.response;
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
- h as F,
202
+ i as F,
206
203
  d as R
207
204
  };
208
- //# sourceMappingURL=CacheHandler-BTU_rYkv.js.map
205
+ //# sourceMappingURL=CacheHandler-BhfbVHed.js.map