@iamjulianacosta/mobx-data 1.0.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.
Files changed (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/CacheHandler-BTU_rYkv.js +208 -0
  4. package/dist/CacheHandler-BTU_rYkv.js.map +1 -0
  5. package/dist/CacheHandler-CXgY9IJo.cjs +2 -0
  6. package/dist/CacheHandler-CXgY9IJo.cjs.map +1 -0
  7. package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +2 -0
  8. package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +1 -0
  9. package/dist/EmbeddedRecordsMixin-VoHluHCT.js +261 -0
  10. package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +1 -0
  11. package/dist/JsonApiSerializer-CC5HXp4b.js +194 -0
  12. package/dist/JsonApiSerializer-CC5HXp4b.js.map +1 -0
  13. package/dist/JsonApiSerializer-CKB02AgP.cjs +2 -0
  14. package/dist/JsonApiSerializer-CKB02AgP.cjs.map +1 -0
  15. package/dist/MemoryAdapter-Bx1e7ndV.js +123 -0
  16. package/dist/MemoryAdapter-Bx1e7ndV.js.map +1 -0
  17. package/dist/MemoryAdapter-D1cTyydm.cjs +2 -0
  18. package/dist/MemoryAdapter-D1cTyydm.cjs.map +1 -0
  19. package/dist/ODataAdapter-C4IHK4BK.js +157 -0
  20. package/dist/ODataAdapter-C4IHK4BK.js.map +1 -0
  21. package/dist/ODataAdapter-DyyF1sdA.cjs +2 -0
  22. package/dist/ODataAdapter-DyyF1sdA.cjs.map +1 -0
  23. package/dist/RestAdapter-B4aRvs4m.js +355 -0
  24. package/dist/RestAdapter-B4aRvs4m.js.map +1 -0
  25. package/dist/RestAdapter-CJOwTsKK.cjs +2 -0
  26. package/dist/RestAdapter-CJOwTsKK.cjs.map +1 -0
  27. package/dist/SchemaService-DZwkFgZu.js +102 -0
  28. package/dist/SchemaService-DZwkFgZu.js.map +1 -0
  29. package/dist/SchemaService-Di_yjVzU.cjs +2 -0
  30. package/dist/SchemaService-Di_yjVzU.cjs.map +1 -0
  31. package/dist/Serializer-95gi5edy.cjs +2 -0
  32. package/dist/Serializer-95gi5edy.cjs.map +1 -0
  33. package/dist/Serializer-FxJbsZ50.js +139 -0
  34. package/dist/Serializer-FxJbsZ50.js.map +1 -0
  35. package/dist/Store-BdwMrbDi.cjs +2 -0
  36. package/dist/Store-BdwMrbDi.cjs.map +1 -0
  37. package/dist/Store-CZ7Z-Nme.js +912 -0
  38. package/dist/Store-CZ7Z-Nme.js.map +1 -0
  39. package/dist/adapter/Adapter.d.ts +146 -0
  40. package/dist/adapter/Adapter.d.ts.map +1 -0
  41. package/dist/adapter/MemoryAdapter.d.ts +44 -0
  42. package/dist/adapter/MemoryAdapter.d.ts.map +1 -0
  43. package/dist/adapter/RestAdapter.d.ts +57 -0
  44. package/dist/adapter/RestAdapter.d.ts.map +1 -0
  45. package/dist/adapter/index.cjs +2 -0
  46. package/dist/adapter/index.cjs.map +1 -0
  47. package/dist/adapter/index.d.ts +4 -0
  48. package/dist/adapter/index.d.ts.map +1 -0
  49. package/dist/adapter/index.js +8 -0
  50. package/dist/adapter/index.js.map +1 -0
  51. package/dist/date-Bj4O2W1F.js +107 -0
  52. package/dist/date-Bj4O2W1F.js.map +1 -0
  53. package/dist/date-CRCe-9gf.cjs +2 -0
  54. package/dist/date-CRCe-9gf.cjs.map +1 -0
  55. package/dist/decorators-HQ1KnRdh.cjs +2 -0
  56. package/dist/decorators-HQ1KnRdh.cjs.map +1 -0
  57. package/dist/decorators-Zr35qr6A.js +50 -0
  58. package/dist/decorators-Zr35qr6A.js.map +1 -0
  59. package/dist/index.cjs +2 -0
  60. package/dist/index.cjs.map +1 -0
  61. package/dist/index.d.ts +10 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +52 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/json-api/JsonApiAdapter.d.ts +38 -0
  66. package/dist/json-api/JsonApiAdapter.d.ts.map +1 -0
  67. package/dist/json-api/JsonApiSerializer.d.ts +73 -0
  68. package/dist/json-api/JsonApiSerializer.d.ts.map +1 -0
  69. package/dist/json-api/index.cjs +2 -0
  70. package/dist/json-api/index.cjs.map +1 -0
  71. package/dist/json-api/index.d.ts +3 -0
  72. package/dist/json-api/index.d.ts.map +1 -0
  73. package/dist/json-api/index.js +6 -0
  74. package/dist/json-api/index.js.map +1 -0
  75. package/dist/model/Errors.d.ts +46 -0
  76. package/dist/model/Errors.d.ts.map +1 -0
  77. package/dist/model/Model.d.ts +226 -0
  78. package/dist/model/Model.d.ts.map +1 -0
  79. package/dist/model/Snapshot.d.ts +72 -0
  80. package/dist/model/Snapshot.d.ts.map +1 -0
  81. package/dist/model/StateMachine.d.ts +45 -0
  82. package/dist/model/StateMachine.d.ts.map +1 -0
  83. package/dist/model/index.cjs +2 -0
  84. package/dist/model/index.cjs.map +1 -0
  85. package/dist/model/index.d.ts +6 -0
  86. package/dist/model/index.d.ts.map +1 -0
  87. package/dist/model/index.js +11 -0
  88. package/dist/model/index.js.map +1 -0
  89. package/dist/model/relationships.d.ts +182 -0
  90. package/dist/model/relationships.d.ts.map +1 -0
  91. package/dist/odata/ODataAdapter.d.ts +67 -0
  92. package/dist/odata/ODataAdapter.d.ts.map +1 -0
  93. package/dist/odata/index.cjs +2 -0
  94. package/dist/odata/index.cjs.map +1 -0
  95. package/dist/odata/index.d.ts +2 -0
  96. package/dist/odata/index.d.ts.map +1 -0
  97. package/dist/odata/index.js +5 -0
  98. package/dist/odata/index.js.map +1 -0
  99. package/dist/relationships-B55LBaCW.cjs +2 -0
  100. package/dist/relationships-B55LBaCW.cjs.map +1 -0
  101. package/dist/relationships-BEXANmWg.js +821 -0
  102. package/dist/relationships-BEXANmWg.js.map +1 -0
  103. package/dist/request/CacheHandler.d.ts +50 -0
  104. package/dist/request/CacheHandler.d.ts.map +1 -0
  105. package/dist/request/FetchHandler.d.ts +41 -0
  106. package/dist/request/FetchHandler.d.ts.map +1 -0
  107. package/dist/request/RequestManager.d.ts +52 -0
  108. package/dist/request/RequestManager.d.ts.map +1 -0
  109. package/dist/request/index.cjs +2 -0
  110. package/dist/request/index.cjs.map +1 -0
  111. package/dist/request/index.d.ts +5 -0
  112. package/dist/request/index.d.ts.map +1 -0
  113. package/dist/request/index.js +7 -0
  114. package/dist/request/index.js.map +1 -0
  115. package/dist/request/types.d.ts +111 -0
  116. package/dist/request/types.d.ts.map +1 -0
  117. package/dist/schema/SchemaService.d.ts +58 -0
  118. package/dist/schema/SchemaService.d.ts.map +1 -0
  119. package/dist/schema/decorators.d.ts +50 -0
  120. package/dist/schema/decorators.d.ts.map +1 -0
  121. package/dist/schema/index.cjs +2 -0
  122. package/dist/schema/index.cjs.map +1 -0
  123. package/dist/schema/index.d.ts +4 -0
  124. package/dist/schema/index.d.ts.map +1 -0
  125. package/dist/schema/index.js +13 -0
  126. package/dist/schema/index.js.map +1 -0
  127. package/dist/schema/types.d.ts +61 -0
  128. package/dist/schema/types.d.ts.map +1 -0
  129. package/dist/serializer/EmbeddedRecordsMixin.d.ts +80 -0
  130. package/dist/serializer/EmbeddedRecordsMixin.d.ts.map +1 -0
  131. package/dist/serializer/JsonSerializer.d.ts +52 -0
  132. package/dist/serializer/JsonSerializer.d.ts.map +1 -0
  133. package/dist/serializer/RestSerializer.d.ts +43 -0
  134. package/dist/serializer/RestSerializer.d.ts.map +1 -0
  135. package/dist/serializer/Serializer.d.ts +202 -0
  136. package/dist/serializer/Serializer.d.ts.map +1 -0
  137. package/dist/serializer/index.cjs +2 -0
  138. package/dist/serializer/index.cjs.map +1 -0
  139. package/dist/serializer/index.d.ts +5 -0
  140. package/dist/serializer/index.d.ts.map +1 -0
  141. package/dist/serializer/index.js +9 -0
  142. package/dist/serializer/index.js.map +1 -0
  143. package/dist/store/IdentityMap.d.ts +53 -0
  144. package/dist/store/IdentityMap.d.ts.map +1 -0
  145. package/dist/store/RecordArray.d.ts +114 -0
  146. package/dist/store/RecordArray.d.ts.map +1 -0
  147. package/dist/store/Store.d.ts +395 -0
  148. package/dist/store/Store.d.ts.map +1 -0
  149. package/dist/store/index.cjs +2 -0
  150. package/dist/store/index.cjs.map +1 -0
  151. package/dist/store/index.d.ts +5 -0
  152. package/dist/store/index.d.ts.map +1 -0
  153. package/dist/store/index.js +8 -0
  154. package/dist/store/index.js.map +1 -0
  155. package/dist/transforms/Transform.d.ts +49 -0
  156. package/dist/transforms/Transform.d.ts.map +1 -0
  157. package/dist/transforms/boolean.d.ts +26 -0
  158. package/dist/transforms/boolean.d.ts.map +1 -0
  159. package/dist/transforms/date.d.ts +22 -0
  160. package/dist/transforms/date.d.ts.map +1 -0
  161. package/dist/transforms/index.cjs +2 -0
  162. package/dist/transforms/index.cjs.map +1 -0
  163. package/dist/transforms/index.d.ts +6 -0
  164. package/dist/transforms/index.d.ts.map +1 -0
  165. package/dist/transforms/index.js +9 -0
  166. package/dist/transforms/index.js.map +1 -0
  167. package/dist/transforms/number.d.ts +17 -0
  168. package/dist/transforms/number.d.ts.map +1 -0
  169. package/dist/transforms/string.d.ts +18 -0
  170. package/dist/transforms/string.d.ts.map +1 -0
  171. package/dist/types-C9NB2gRj.js +7 -0
  172. package/dist/types-C9NB2gRj.js.map +1 -0
  173. package/dist/types-uWOXMPWW.cjs +2 -0
  174. package/dist/types-uWOXMPWW.cjs.map +1 -0
  175. package/package.json +140 -0
  176. package/src/adapter/Adapter.ts +320 -0
  177. package/src/adapter/MemoryAdapter.ts +216 -0
  178. package/src/adapter/RestAdapter.ts +248 -0
  179. package/src/adapter/index.ts +7 -0
  180. package/src/index.ts +17 -0
  181. package/src/json-api/JsonApiAdapter.ts +93 -0
  182. package/src/json-api/JsonApiSerializer.ts +245 -0
  183. package/src/json-api/index.ts +2 -0
  184. package/src/model/Errors.ts +100 -0
  185. package/src/model/Model.ts +683 -0
  186. package/src/model/Snapshot.ts +162 -0
  187. package/src/model/StateMachine.ts +149 -0
  188. package/src/model/index.ts +20 -0
  189. package/src/model/relationships.ts +484 -0
  190. package/src/odata/ODataAdapter.ts +245 -0
  191. package/src/odata/index.ts +1 -0
  192. package/src/request/CacheHandler.ts +125 -0
  193. package/src/request/FetchHandler.ts +119 -0
  194. package/src/request/RequestManager.ts +112 -0
  195. package/src/request/index.ts +4 -0
  196. package/src/request/types.ts +139 -0
  197. package/src/schema/SchemaService.ts +161 -0
  198. package/src/schema/decorators.ts +162 -0
  199. package/src/schema/index.ts +3 -0
  200. package/src/schema/types.ts +66 -0
  201. package/src/serializer/EmbeddedRecordsMixin.ts +257 -0
  202. package/src/serializer/JsonSerializer.ts +173 -0
  203. package/src/serializer/RestSerializer.ts +138 -0
  204. package/src/serializer/Serializer.ts +397 -0
  205. package/src/serializer/index.ts +15 -0
  206. package/src/store/IdentityMap.ts +110 -0
  207. package/src/store/RecordArray.ts +210 -0
  208. package/src/store/Store.ts +1391 -0
  209. package/src/store/index.ts +11 -0
  210. package/src/transforms/Transform.ts +52 -0
  211. package/src/transforms/boolean.ts +57 -0
  212. package/src/transforms/date.ts +48 -0
  213. package/src/transforms/index.ts +5 -0
  214. package/src/transforms/number.ts +42 -0
  215. package/src/transforms/string.ts +35 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Abstract base class for all serializers.
3
+ *
4
+ * A serializer translates between the raw wire format returned by an adapter
5
+ * and the normalized document format consumed by the `Store`. It also
6
+ * converts a record `Snapshot` back into a wire-format payload for create /
7
+ * update requests.
8
+ *
9
+ * ## Normalization pipeline
10
+ *
11
+ * ```
12
+ * raw payload
13
+ * → normalizeResponse() (dispatches to the per-operation override)
14
+ * → normalizeFindRecordResponse() / normalizeQueryResponse() / …
15
+ * → normalize() (normalizes a single resource hash)
16
+ * → extractId()
17
+ * → extractAttributes()
18
+ * → extractRelationships()
19
+ * → NormalizedDocument
20
+ * ```
21
+ *
22
+ * ## Serialization pipeline
23
+ *
24
+ * ```
25
+ * Snapshot
26
+ * → serialize()
27
+ * → serializeAttribute() (for each @attr)
28
+ * → serializeBelongsTo() (for each @belongsTo)
29
+ * → serializeHasMany() (for each @hasMany)
30
+ * → wire payload object
31
+ * ```
32
+ *
33
+ * Subclasses (`JsonSerializer`, `RestSerializer`, `JsonApiSerializer`) override
34
+ * selected methods to handle their specific wire formats.
35
+ */
36
+
37
+ import type { AttributeDef, RelationshipDef } from '@mobx-data/schema';
38
+
39
+ /** Union of all request types that trigger a normalization call. */
40
+ export type NormalizeRequestType =
41
+ | 'findRecord'
42
+ | 'findAll'
43
+ | 'findBelongsTo'
44
+ | 'findHasMany'
45
+ | 'findMany'
46
+ | 'query'
47
+ | 'queryRecord'
48
+ | 'createRecord'
49
+ | 'updateRecord'
50
+ | 'deleteRecord';
51
+
52
+ /**
53
+ * Normalized representation of a single server resource.
54
+ * This is the internal format understood by the `Store`.
55
+ */
56
+ export interface NormalizedResource {
57
+ /** Model name. */
58
+ type: string;
59
+ /** Server-assigned id, or `null` for new records. */
60
+ id: string | null;
61
+ /** Flat attribute map keyed by property name. */
62
+ attributes?: Record<string, unknown>;
63
+ /**
64
+ * Relationship references keyed by property name.
65
+ * Each value contains `data` — a single `{ type, id }` or an array of them.
66
+ */
67
+ relationships?: Record<
68
+ string,
69
+ {
70
+ data:
71
+ | { type: string; id: string }
72
+ | Array<{ type: string; id: string }>
73
+ | null;
74
+ }
75
+ >;
76
+ }
77
+
78
+ /**
79
+ * Top-level normalized document returned by `normalizeResponse`.
80
+ * Mirrors the JSON:API document structure but is also used for REST payloads.
81
+ */
82
+ export interface NormalizedDocument {
83
+ /** Primary resource(s), or `null` for empty responses. */
84
+ data: NormalizedResource | NormalizedResource[] | null;
85
+ /** Side-loaded or compound-document secondary resources. */
86
+ included?: NormalizedResource[];
87
+ /** Server-side metadata (pagination, total counts, etc.). */
88
+ meta?: Record<string, unknown>;
89
+ /** Pagination or related links. */
90
+ links?: Record<string, string>;
91
+ }
92
+
93
+ /**
94
+ * Snapshot-like interface consumed by `serialize` and the `serialize*` helpers.
95
+ * Serializers only read from snapshots; they never mutate records.
96
+ */
97
+ export interface SerializerSnapshot {
98
+ /** Server-assigned id, or `null` for new records. */
99
+ id: string | null;
100
+ /** Model name. */
101
+ modelName: string;
102
+ /** Returns the snapshot-time value for an attribute key. */
103
+ attr(key: string): unknown;
104
+ /** Returns the `belongsTo` reference (or just the id when `{ id: true }`). */
105
+ belongsTo(key: string, options?: { id: boolean }): unknown;
106
+ /** Returns the `hasMany` references (or just ids when `{ ids: true }`). */
107
+ hasMany(key: string, options?: { ids: boolean }): unknown;
108
+ /** Returns `{ [key]: [original, current] }` pairs for dirty attributes. */
109
+ changedAttributes(): Record<string, [unknown, unknown]>;
110
+ /** Iterates over all attribute definitions. */
111
+ eachAttribute(fn: (key: string, meta: AttributeDef) => void): void;
112
+ /** Iterates over all relationship definitions. */
113
+ eachRelationship(fn: (key: string, meta: RelationshipDef) => void): void;
114
+ /** Live record reference (treated as read-only in serializer context). */
115
+ record: unknown;
116
+ }
117
+
118
+ /** Minimal model class descriptor passed to serializer methods. */
119
+ export interface ModelClassMeta {
120
+ /** Registered model name. */
121
+ modelName: string;
122
+ /** Merged attribute definitions. */
123
+ attributes: Map<string, AttributeDef>;
124
+ /** Merged relationship definitions. */
125
+ relationships: Map<string, RelationshipDef>;
126
+ }
127
+
128
+ export abstract class Serializer {
129
+ /** Name of the field used as the primary key in raw payloads. Default: `'id'`. */
130
+ primaryKey: string = 'id';
131
+
132
+ /**
133
+ * Normalizes a single raw resource hash into a `NormalizedResource`.
134
+ * Returns `null` for absent or non-object payloads.
135
+ */
136
+ abstract normalize(
137
+ store: unknown,
138
+ modelClass: ModelClassMeta,
139
+ payload: unknown,
140
+ prop?: string,
141
+ ): NormalizedResource | null;
142
+
143
+ /**
144
+ * Entry point for normalization. Dispatches to the appropriate
145
+ * `normalize*Response` method based on `requestType`, then builds the full
146
+ * `NormalizedDocument`.
147
+ */
148
+ abstract normalizeResponse(
149
+ store: unknown,
150
+ modelClass: ModelClassMeta,
151
+ payload: unknown,
152
+ id: string | null,
153
+ requestType: NormalizeRequestType,
154
+ ): NormalizedDocument;
155
+
156
+ /**
157
+ * Serializes a record snapshot into a plain object suitable for a create
158
+ * or update request body.
159
+ */
160
+ abstract serialize(
161
+ snapshot: SerializerSnapshot,
162
+ options?: { includeId?: boolean },
163
+ ): Record<string, unknown>;
164
+
165
+ // Per-operation normalization hooks — default to calling `normalizeResponse`.
166
+
167
+ /** Called when normalizing a `findRecord` response. */
168
+ normalizeFindRecordResponse(
169
+ store: unknown,
170
+ modelClass: ModelClassMeta,
171
+ payload: unknown,
172
+ id: string | null,
173
+ requestType: NormalizeRequestType,
174
+ ): NormalizedDocument {
175
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
176
+ }
177
+
178
+ /** Called when normalizing a `findAll` response. */
179
+ normalizeFindAllResponse(
180
+ store: unknown,
181
+ modelClass: ModelClassMeta,
182
+ payload: unknown,
183
+ id: string | null,
184
+ requestType: NormalizeRequestType,
185
+ ): NormalizedDocument {
186
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
187
+ }
188
+
189
+ /** Called when normalizing a `query` response. */
190
+ normalizeQueryResponse(
191
+ store: unknown,
192
+ modelClass: ModelClassMeta,
193
+ payload: unknown,
194
+ id: string | null,
195
+ requestType: NormalizeRequestType,
196
+ ): NormalizedDocument {
197
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
198
+ }
199
+
200
+ /** Called when normalizing a `queryRecord` response. */
201
+ normalizeQueryRecordResponse(
202
+ store: unknown,
203
+ modelClass: ModelClassMeta,
204
+ payload: unknown,
205
+ id: string | null,
206
+ requestType: NormalizeRequestType,
207
+ ): NormalizedDocument {
208
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
209
+ }
210
+
211
+ /** Called when normalizing a `createRecord` response. */
212
+ normalizeCreateRecordResponse(
213
+ store: unknown,
214
+ modelClass: ModelClassMeta,
215
+ payload: unknown,
216
+ id: string | null,
217
+ requestType: NormalizeRequestType,
218
+ ): NormalizedDocument {
219
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
220
+ }
221
+
222
+ /** Called when normalizing an `updateRecord` response. */
223
+ normalizeUpdateRecordResponse(
224
+ store: unknown,
225
+ modelClass: ModelClassMeta,
226
+ payload: unknown,
227
+ id: string | null,
228
+ requestType: NormalizeRequestType,
229
+ ): NormalizedDocument {
230
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
231
+ }
232
+
233
+ /** Called when normalizing a `deleteRecord` response. */
234
+ normalizeDeleteRecordResponse(
235
+ store: unknown,
236
+ modelClass: ModelClassMeta,
237
+ payload: unknown,
238
+ id: string | null,
239
+ requestType: NormalizeRequestType,
240
+ ): NormalizedDocument {
241
+ return this.normalizeResponse(store, modelClass, payload, id, requestType);
242
+ }
243
+
244
+ /**
245
+ * Writes a single attribute value into `json`.
246
+ * Default: writes `snapshot.attr(key)` under the key returned by `keyForAttribute`.
247
+ */
248
+ serializeAttribute(
249
+ snapshot: SerializerSnapshot,
250
+ json: Record<string, unknown>,
251
+ key: string,
252
+ _attribute: AttributeDef,
253
+ ): void {
254
+ json[this.keyForAttribute(key)] = snapshot.attr(key);
255
+ }
256
+
257
+ /**
258
+ * Writes a `belongsTo` relationship id into `json`.
259
+ * Default: writes the related record's id (or `null`) under `keyForRelationship`.
260
+ */
261
+ serializeBelongsTo(
262
+ snapshot: SerializerSnapshot,
263
+ json: Record<string, unknown>,
264
+ relationship: RelationshipDef,
265
+ ): void {
266
+ const id = snapshot.belongsTo(relationship.name, { id: true }) as
267
+ | string
268
+ | null;
269
+ json[this.keyForRelationship(relationship.name)] = id ?? null;
270
+ }
271
+
272
+ /**
273
+ * Writes a `hasMany` relationship id array into `json`.
274
+ * Default: writes the array of related ids under `keyForRelationship`.
275
+ */
276
+ serializeHasMany(
277
+ snapshot: SerializerSnapshot,
278
+ json: Record<string, unknown>,
279
+ relationship: RelationshipDef,
280
+ ): void {
281
+ const ids = snapshot.hasMany(relationship.name, { ids: true }) as string[];
282
+ json[this.keyForRelationship(relationship.name)] = ids ?? [];
283
+ }
284
+
285
+ /**
286
+ * Extracts attribute values from a raw resource hash.
287
+ * Returns a map of `{ propertyName: value }` using `keyForAttribute` to
288
+ * locate the payload key.
289
+ */
290
+ extractAttributes(
291
+ modelClass: ModelClassMeta,
292
+ resourceHash: Record<string, unknown>,
293
+ ): Record<string, unknown> {
294
+ const attributes: Record<string, unknown> = {};
295
+ for (const [name] of modelClass.attributes) {
296
+ const payloadKey = this.keyForAttribute(name);
297
+ if (payloadKey in resourceHash) {
298
+ attributes[name] = resourceHash[payloadKey];
299
+ }
300
+ }
301
+ return attributes;
302
+ }
303
+
304
+ /**
305
+ * Extracts relationship references from a raw resource hash.
306
+ *
307
+ * - `belongsTo`: raw id (string or number) → `{ data: { type, id } }`
308
+ * - `hasMany`: array of raw ids → `{ data: [{ type, id }, …] }`
309
+ *
310
+ * Returns only the relationships whose payload keys are present in `resourceHash`.
311
+ */
312
+ extractRelationships(
313
+ modelClass: ModelClassMeta,
314
+ resourceHash: Record<string, unknown>,
315
+ ): NormalizedResource['relationships'] {
316
+ const relationships: NonNullable<NormalizedResource['relationships']> = {};
317
+ for (const [name, rel] of modelClass.relationships) {
318
+ const payloadKey = this.keyForRelationship(name);
319
+ if (!(payloadKey in resourceHash)) {
320
+ continue;
321
+ }
322
+ const raw = resourceHash[payloadKey];
323
+ if (rel.kind === 'belongsTo') {
324
+ if (raw === null || raw === undefined) {
325
+ relationships[name] = { data: null };
326
+ } else if (typeof raw === 'string' || typeof raw === 'number') {
327
+ relationships[name] = {
328
+ data: { type: rel.type, id: String(raw) },
329
+ };
330
+ }
331
+ } else if (Array.isArray(raw)) {
332
+ const ids = raw
333
+ .filter((entry) => typeof entry === 'string' || typeof entry === 'number')
334
+ .map((entry) => ({ type: rel.type, id: String(entry) }));
335
+ if (ids.length === raw.length) {
336
+ relationships[name] = { data: ids };
337
+ }
338
+ }
339
+ }
340
+ return relationships;
341
+ }
342
+
343
+ /**
344
+ * Extracts the primary key from a raw resource hash.
345
+ * Returns `null` when the key is absent.
346
+ */
347
+ extractId(
348
+ _modelClass: ModelClassMeta,
349
+ resourceHash: Record<string, unknown>,
350
+ ): string | null {
351
+ const raw = resourceHash[this.primaryKey];
352
+ if (raw === null || raw === undefined) {
353
+ return null;
354
+ }
355
+ return String(raw);
356
+ }
357
+
358
+ /**
359
+ * Extracts field-level validation errors from a server error payload.
360
+ * Default: looks for `{ errors: { field: string | string[] } }`.
361
+ * Returns an empty object when no recognisable error structure is found.
362
+ */
363
+ extractErrors(
364
+ _store: unknown,
365
+ _modelClass: ModelClassMeta,
366
+ payload: unknown,
367
+ _id: string | null,
368
+ ): Record<string, string[]> {
369
+ if (payload && typeof payload === 'object' && 'errors' in payload) {
370
+ const errs = (payload as { errors: unknown }).errors;
371
+ if (errs && typeof errs === 'object' && !Array.isArray(errs)) {
372
+ const out: Record<string, string[]> = {};
373
+ for (const [attribute, value] of Object.entries(errs as Record<string, unknown>)) {
374
+ out[attribute] = Array.isArray(value) ? value.map(String) : [String(value)];
375
+ }
376
+ return out;
377
+ }
378
+ }
379
+ return {};
380
+ }
381
+
382
+ /**
383
+ * Maps a camelCase property name to the payload key used by this format.
384
+ * Default: identity (no transformation).
385
+ */
386
+ keyForAttribute(key: string): string {
387
+ return key;
388
+ }
389
+
390
+ /**
391
+ * Maps a camelCase relationship name to the payload key used by this format.
392
+ * Default: identity (no transformation).
393
+ */
394
+ keyForRelationship(key: string): string {
395
+ return key;
396
+ }
397
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ Serializer,
3
+ type NormalizedDocument,
4
+ type NormalizedResource,
5
+ type NormalizeRequestType,
6
+ type SerializerSnapshot,
7
+ type ModelClassMeta,
8
+ } from './Serializer.js';
9
+ export { RestSerializer } from './RestSerializer.js';
10
+ export { JsonSerializer } from './JsonSerializer.js';
11
+ export {
12
+ EmbeddedRecordsMixin,
13
+ type EmbeddedAttrConfig,
14
+ type EmbeddedRecordsAttrs,
15
+ } from './EmbeddedRecordsMixin.js';
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Two-level observable map that serves as the store's record cache.
3
+ *
4
+ * Records are stored in per-type buckets:
5
+ * ```
6
+ * buckets: Map<modelName, Map<id, Model>>
7
+ * ```
8
+ *
9
+ * The outer map is MobX-observable (shallow) so derived views like `peekAll`
10
+ * react when new type buckets are added. Each inner bucket is an observable
11
+ * map so computed properties that iterate records within a type react to
12
+ * additions and deletions.
13
+ *
14
+ * All mutating methods (`set`, `delete`, `clear`) are MobX `action`s so they
15
+ * batch observable updates correctly.
16
+ */
17
+
18
+ import { makeObservable, observable, action } from 'mobx';
19
+ import type { Model } from '@mobx-data/model';
20
+
21
+ export class IdentityMap {
22
+ /** @internal */
23
+ readonly _buckets: Map<string, Map<string, Model>> = new Map();
24
+
25
+ constructor() {
26
+ makeObservable<this, '_buckets'>(this, {
27
+ _buckets: observable.shallow,
28
+ set: action,
29
+ delete: action,
30
+ clear: action,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Returns the bucket for `modelName`, optionally creating it when absent.
36
+ * Internal helper — not part of the public API.
37
+ */
38
+ private bucket(modelName: string, create = false): Map<string, Model> | undefined {
39
+ let existing = this._buckets.get(modelName);
40
+ if (!existing && create) {
41
+ existing = observable.map<string, Model>({}, { deep: false });
42
+ this._buckets.set(modelName, existing);
43
+ }
44
+ return existing;
45
+ }
46
+
47
+ /** Adds or replaces the record with the given `id` under `modelName`. */
48
+ set(modelName: string, id: string, record: Model): void {
49
+ const bucket = this.bucket(modelName, true)!;
50
+ bucket.set(id, record);
51
+ }
52
+
53
+ /**
54
+ * Returns the record for `modelName` + `id`, or `null` when not found.
55
+ */
56
+ get(modelName: string, id: string): Model | null {
57
+ return this.bucket(modelName)?.get(id) ?? null;
58
+ }
59
+
60
+ /** Returns `true` when a record exists for `modelName` + `id`. */
61
+ has(modelName: string, id: string): boolean {
62
+ return this.bucket(modelName)?.has(id) ?? false;
63
+ }
64
+
65
+ /**
66
+ * Removes the record for `modelName` + `id`.
67
+ * @returns `true` when the record existed and was deleted.
68
+ */
69
+ delete(modelName: string, id: string): boolean {
70
+ return this.bucket(modelName)?.delete(id) ?? false;
71
+ }
72
+
73
+ /** Returns all records stored under `modelName` as an array. */
74
+ all(modelName: string): Model[] {
75
+ const bucket = this.bucket(modelName);
76
+ if (!bucket) {
77
+ return [];
78
+ }
79
+ return Array.from(bucket.values());
80
+ }
81
+
82
+ /**
83
+ * Clears all records for `modelName`, or all records across all types when
84
+ * `modelName` is omitted.
85
+ */
86
+ clear(modelName?: string): void {
87
+ if (modelName) {
88
+ this.bucket(modelName)?.clear();
89
+ } else {
90
+ for (const bucket of this._buckets.values()) {
91
+ bucket.clear();
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Returns the number of records stored for `modelName`, or the total across
98
+ * all types when `modelName` is omitted.
99
+ */
100
+ size(modelName?: string): number {
101
+ if (modelName) {
102
+ return this.bucket(modelName)?.size ?? 0;
103
+ }
104
+ let total = 0;
105
+ for (const bucket of this._buckets.values()) {
106
+ total += bucket.size;
107
+ }
108
+ return total;
109
+ }
110
+ }