@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,257 @@
1
+ /**
2
+ * Mixin that adds support for embedded (nested) records in REST payloads.
3
+ *
4
+ * Apply with:
5
+ * ```ts
6
+ * class PostSerializer extends EmbeddedRecordsMixin(RestSerializer) {
7
+ * attrs = {
8
+ * comments: { embedded: 'always' },
9
+ * author: { embedded: 'always', serialize: 'records' },
10
+ * };
11
+ * }
12
+ * ```
13
+ *
14
+ * ## How it works
15
+ *
16
+ * ### Deserialization
17
+ * When `embedded: 'always'` is set for a relationship, `normalize()` intercepts
18
+ * the raw payload before passing it to `super.normalize()`:
19
+ * - For `hasMany`: replaces the nested array with a plain id array and queues
20
+ * each nested object as an `included` resource.
21
+ * - For `belongsTo`: replaces the nested object with the extracted id and
22
+ * queues the nested object as an `included` resource.
23
+ *
24
+ * Queued resources are collected during `normalizeResponse()` and appended to
25
+ * `document.included` so the `Store` can push them into the identity map.
26
+ *
27
+ * ### Serialization
28
+ * When `serialize: 'records'` is set, `serializeHasMany` / `serializeBelongsTo`
29
+ * write the full related record(s) into the payload instead of just ids.
30
+ *
31
+ * ## Configuration
32
+ *
33
+ * Each entry in `attrs` is an `EmbeddedAttrConfig`:
34
+ * - `embedded: 'always'` — enable embedded deserialization.
35
+ * - `serialize: 'records'` — write full records on serialize.
36
+ * - `serialize: 'ids'` — write id array on serialize (default).
37
+ * - `serialize: false` — omit relationship from serialized payload.
38
+ * - `deserialize: 'records'` — same as `embedded: 'always'`.
39
+ * - `deserialize: false` — ignore this relationship during normalization.
40
+ */
41
+
42
+ import type { RelationshipDef } from '@mobx-data/schema';
43
+ import type {
44
+ ModelClassMeta,
45
+ NormalizeRequestType,
46
+ NormalizedDocument,
47
+ NormalizedResource,
48
+ SerializerSnapshot,
49
+ } from './Serializer.js';
50
+
51
+ /** Per-relationship embedding configuration. */
52
+ export interface EmbeddedAttrConfig {
53
+ /** `'always'` to deserialize embedded records; `'never'` to skip. */
54
+ embedded?: 'always' | 'never';
55
+ /** `'records'` to serialize full objects; `'ids'` for ids only; `false` to omit. */
56
+ serialize?: 'records' | 'ids' | false;
57
+ /** `'records'` to deserialize embedded objects; `'ids'` for plain ids; `false` to skip. */
58
+ deserialize?: 'records' | 'ids' | false;
59
+ }
60
+
61
+ /** Map of relationship name → embedding config. */
62
+ export interface EmbeddedRecordsAttrs {
63
+ [key: string]: EmbeddedAttrConfig;
64
+ }
65
+
66
+ /**
67
+ * The minimal serializer interface required by the mixin.
68
+ * Any class extending `JsonSerializer` satisfies this.
69
+ */
70
+ export interface JsonSerializerLike {
71
+ attrs?: EmbeddedRecordsAttrs;
72
+ primaryKey: string;
73
+ normalize(
74
+ store: unknown,
75
+ modelClass: ModelClassMeta,
76
+ payload: unknown,
77
+ prop?: string,
78
+ ): NormalizedResource | null;
79
+ normalizeResponse(
80
+ store: unknown,
81
+ modelClass: ModelClassMeta,
82
+ payload: unknown,
83
+ id: string | null,
84
+ requestType: NormalizeRequestType,
85
+ ): NormalizedDocument;
86
+ serialize(
87
+ snapshot: SerializerSnapshot,
88
+ options?: { includeId?: boolean },
89
+ ): Record<string, unknown>;
90
+ serializeHasMany(
91
+ snapshot: SerializerSnapshot,
92
+ json: Record<string, unknown>,
93
+ relationship: RelationshipDef,
94
+ ): void;
95
+ serializeBelongsTo(
96
+ snapshot: SerializerSnapshot,
97
+ json: Record<string, unknown>,
98
+ relationship: RelationshipDef,
99
+ ): void;
100
+ keyForAttribute(key: string): string;
101
+ keyForRelationship(key: string): string;
102
+ }
103
+
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ type Constructor<T> = new (...args: any[]) => T;
106
+
107
+ /**
108
+ * Returns a new class that extends `Base` with embedded-record support.
109
+ * `Base` must be (or extend) `JsonSerializer`.
110
+ */
111
+ export function EmbeddedRecordsMixin<
112
+ TBase extends Constructor<JsonSerializerLike>,
113
+ >(Base: TBase): TBase {
114
+ class WithEmbeddedRecords extends Base {
115
+ override attrs?: EmbeddedRecordsAttrs;
116
+
117
+ /** Accumulates extracted embedded resources during a `normalizeResponse` call. */
118
+ private pendingIncluded: NormalizedResource[] = [];
119
+
120
+ /**
121
+ * Intercepts raw payload hashes to extract embedded records.
122
+ *
123
+ * For each relationship configured with `embedded: 'always'`:
124
+ * - Recursively normalizes the embedded object(s) and adds them to
125
+ * `pendingIncluded`.
126
+ * - Replaces the embedded value in the hash with the extracted id(s) so
127
+ * `super.normalize` treats it as a plain id reference.
128
+ */
129
+ override normalize(
130
+ store: unknown,
131
+ modelClass: ModelClassMeta,
132
+ payload: unknown,
133
+ prop?: string,
134
+ ): NormalizedResource | null {
135
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
136
+ return super.normalize(store, modelClass, payload, prop);
137
+ }
138
+ const src = payload as Record<string, unknown>;
139
+ const attrs = this.attrs ?? {};
140
+ const hash: Record<string, unknown> = { ...src };
141
+
142
+ for (const [name, rel] of modelClass.relationships) {
143
+ const cfg = attrs[name];
144
+ if (!cfg || cfg.embedded !== 'always') {
145
+ continue;
146
+ }
147
+ const raw = hash[name];
148
+ if (raw === undefined || raw === null) {
149
+ continue;
150
+ }
151
+
152
+ if (rel.kind === 'hasMany' && Array.isArray(raw)) {
153
+ const ids: string[] = [];
154
+ for (const item of raw) {
155
+ const extracted = this.extractEmbeddedResource(store, rel, item);
156
+ if (extracted && extracted.id !== null) {
157
+ ids.push(extracted.id);
158
+ this.pendingIncluded.push(extracted);
159
+ }
160
+ }
161
+ hash[name] = ids;
162
+ } else if (
163
+ rel.kind === 'belongsTo'
164
+ && typeof raw === 'object'
165
+ && !Array.isArray(raw)
166
+ ) {
167
+ const extracted = this.extractEmbeddedResource(store, rel, raw);
168
+ if (extracted && extracted.id !== null) {
169
+ this.pendingIncluded.push(extracted);
170
+ hash[name] = extracted.id;
171
+ }
172
+ }
173
+ }
174
+
175
+ return super.normalize(store, modelClass, hash, prop);
176
+ }
177
+
178
+ /**
179
+ * Resets `pendingIncluded`, delegates to `super.normalizeResponse`, then
180
+ * appends any extracted embedded resources to `document.included`.
181
+ */
182
+ override normalizeResponse(
183
+ store: unknown,
184
+ modelClass: ModelClassMeta,
185
+ payload: unknown,
186
+ id: string | null,
187
+ requestType: NormalizeRequestType,
188
+ ): NormalizedDocument {
189
+ this.pendingIncluded = [];
190
+ const doc = super.normalizeResponse(store, modelClass, payload, id, requestType);
191
+ if (this.pendingIncluded.length > 0) {
192
+ const included = doc.included ? [...doc.included] : [];
193
+ included.push(...this.pendingIncluded);
194
+ doc.included = included;
195
+ this.pendingIncluded = [];
196
+ }
197
+ return doc;
198
+ }
199
+
200
+ /**
201
+ * When `serialize: 'records'` is configured, writes the full related
202
+ * record objects instead of just ids.
203
+ */
204
+ override serializeHasMany(
205
+ snapshot: SerializerSnapshot,
206
+ json: Record<string, unknown>,
207
+ relationship: RelationshipDef,
208
+ ): void {
209
+ const cfg = this.attrs?.[relationship.name];
210
+ if (cfg?.serialize === 'records') {
211
+ const records = snapshot.hasMany(relationship.name) as unknown[];
212
+ json[this.keyForRelationship(relationship.name)] = records ?? [];
213
+ return;
214
+ }
215
+ super.serializeHasMany(snapshot, json, relationship);
216
+ }
217
+
218
+ /**
219
+ * When `serialize: 'records'` is configured, writes the full related
220
+ * record object instead of just the id.
221
+ */
222
+ override serializeBelongsTo(
223
+ snapshot: SerializerSnapshot,
224
+ json: Record<string, unknown>,
225
+ relationship: RelationshipDef,
226
+ ): void {
227
+ const cfg = this.attrs?.[relationship.name];
228
+ if (cfg?.serialize === 'records') {
229
+ const record = snapshot.belongsTo(relationship.name);
230
+ json[this.keyForRelationship(relationship.name)] = record ?? null;
231
+ return;
232
+ }
233
+ super.serializeBelongsTo(snapshot, json, relationship);
234
+ }
235
+
236
+ /**
237
+ * Normalizes a single embedded resource object using the related model's
238
+ * type as a placeholder `ModelClassMeta`.
239
+ */
240
+ private extractEmbeddedResource(
241
+ store: unknown,
242
+ rel: RelationshipDef,
243
+ item: unknown,
244
+ ): NormalizedResource | null {
245
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
246
+ return null;
247
+ }
248
+ const embeddedClass: ModelClassMeta = {
249
+ modelName: rel.type,
250
+ attributes: new Map(),
251
+ relationships: new Map(),
252
+ };
253
+ return this.normalize(store, embeddedClass, item);
254
+ }
255
+ }
256
+ return WithEmbeddedRecords as unknown as TBase;
257
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Flat-JSON serializer for simple REST APIs.
3
+ *
4
+ * `JsonSerializer` expects payloads to be plain JSON objects or arrays with
5
+ * no root-key wrapping. It is the lowest-level concrete serializer and the
6
+ * base that `RestSerializer` extends.
7
+ *
8
+ * Normalization:
9
+ * - Array payload → `{ data: [NormalizedResource, …] }`
10
+ * - Object payload → `{ data: NormalizedResource }`
11
+ * - `null` / `undefined` → `{ data: null }`
12
+ *
13
+ * Serialization:
14
+ * - Iterates `@attr` → writes via `serializeAttribute`
15
+ * - Iterates `@belongsTo` / `@hasMany` → writes via `serializeBelongsTo` /
16
+ * `serializeHasMany`
17
+ *
18
+ * `normalizeResponse` automatically dispatches to the correct per-operation
19
+ * hook (`normalizeFindRecordResponse`, etc.) when a subclass overrides them.
20
+ * If no override exists it falls through to `_buildDocument`.
21
+ */
22
+
23
+ import { injectable } from 'tsyringe';
24
+ import {
25
+ Serializer,
26
+ type ModelClassMeta,
27
+ type NormalizeRequestType,
28
+ type NormalizedDocument,
29
+ type NormalizedResource,
30
+ type SerializerSnapshot,
31
+ } from './Serializer.js';
32
+
33
+ @injectable()
34
+ export class JsonSerializer extends Serializer {
35
+ static dispatchMethodName(
36
+ requestType: NormalizeRequestType,
37
+ ): keyof Serializer | null {
38
+ switch (requestType) {
39
+ case 'findRecord':
40
+ return 'normalizeFindRecordResponse';
41
+ case 'findAll':
42
+ return 'normalizeFindAllResponse';
43
+ case 'query':
44
+ return 'normalizeQueryResponse';
45
+ case 'queryRecord':
46
+ return 'normalizeQueryRecordResponse';
47
+ case 'createRecord':
48
+ return 'normalizeCreateRecordResponse';
49
+ case 'updateRecord':
50
+ return 'normalizeUpdateRecordResponse';
51
+ case 'deleteRecord':
52
+ return 'normalizeDeleteRecordResponse';
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Normalizes a single flat JSON object into a `NormalizedResource`.
60
+ * Returns `null` for absent or non-object payloads.
61
+ */
62
+ override normalize(
63
+ _store: unknown,
64
+ modelClass: ModelClassMeta,
65
+ payload: unknown,
66
+ _prop?: string,
67
+ ): NormalizedResource | null {
68
+ if (payload === null || payload === undefined) {
69
+ return null;
70
+ }
71
+ if (typeof payload !== 'object') {
72
+ return null;
73
+ }
74
+ const hash = payload as Record<string, unknown>;
75
+ const id = this.extractId(modelClass, hash);
76
+ const attributes = this.extractAttributes(modelClass, hash);
77
+ const relationships = this.extractRelationships(modelClass, hash);
78
+
79
+ const resource: NormalizedResource = {
80
+ type: modelClass.modelName,
81
+ id,
82
+ attributes,
83
+ };
84
+ if (relationships && Object.keys(relationships).length > 0) {
85
+ resource.relationships = relationships;
86
+ }
87
+ return resource;
88
+ }
89
+
90
+ /**
91
+ * Entry point for normalization.
92
+ *
93
+ * Checks whether a subclass has overridden the relevant per-operation hook
94
+ * (e.g. `normalizeFindRecordResponse`). If so, calls it; otherwise falls
95
+ * through to `_buildDocument`.
96
+ */
97
+ override normalizeResponse(
98
+ store: unknown,
99
+ modelClass: ModelClassMeta,
100
+ payload: unknown,
101
+ id: string | null,
102
+ requestType: NormalizeRequestType,
103
+ ): NormalizedDocument {
104
+ const methodName = JsonSerializer.dispatchMethodName(requestType);
105
+ if (methodName) {
106
+ const override = (this as unknown as Record<string, unknown>)[methodName];
107
+ const baseImpl = (Serializer.prototype as unknown as Record<string, unknown>)[
108
+ methodName
109
+ ];
110
+ if (typeof override === 'function' && override !== baseImpl) {
111
+ return (override as (...args: unknown[]) => NormalizedDocument).call(
112
+ this,
113
+ store,
114
+ modelClass,
115
+ payload,
116
+ id,
117
+ requestType,
118
+ );
119
+ }
120
+ }
121
+ return this._buildDocument(store, modelClass, payload, id, requestType);
122
+ }
123
+
124
+ /**
125
+ * Builds a `NormalizedDocument` from a raw payload.
126
+ * Arrays are normalized item by item; plain objects are normalized as a
127
+ * single resource.
128
+ */
129
+ protected _buildDocument(
130
+ store: unknown,
131
+ modelClass: ModelClassMeta,
132
+ payload: unknown,
133
+ _id: string | null,
134
+ _requestType: NormalizeRequestType,
135
+ ): NormalizedDocument {
136
+ if (payload === null || payload === undefined) {
137
+ return { data: null };
138
+ }
139
+ if (Array.isArray(payload)) {
140
+ const data = payload
141
+ .map((p) => this.normalize(store, modelClass, p))
142
+ .filter((r): r is NormalizedResource => r !== null);
143
+ return { data };
144
+ }
145
+ const data = this.normalize(store, modelClass, payload);
146
+ return { data };
147
+ }
148
+
149
+ /**
150
+ * Serializes a snapshot to a flat JSON object.
151
+ * Includes `id` when `options.includeId` is `true`.
152
+ */
153
+ override serialize(
154
+ snapshot: SerializerSnapshot,
155
+ options?: { includeId?: boolean },
156
+ ): Record<string, unknown> {
157
+ const json: Record<string, unknown> = {};
158
+ if (options?.includeId && snapshot.id !== null) {
159
+ json[this.primaryKey] = snapshot.id;
160
+ }
161
+ snapshot.eachAttribute((key, meta) => {
162
+ this.serializeAttribute(snapshot, json, key, meta);
163
+ });
164
+ snapshot.eachRelationship((_key, rel) => {
165
+ if (rel.kind === 'belongsTo') {
166
+ this.serializeBelongsTo(snapshot, json, rel);
167
+ } else {
168
+ this.serializeHasMany(snapshot, json, rel);
169
+ }
170
+ });
171
+ return json;
172
+ }
173
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * REST serializer for root-key payloads with optional sideloading.
3
+ *
4
+ * `RestSerializer` expects the primary data to be wrapped under a root key
5
+ * that matches either the singular or plural model name:
6
+ *
7
+ * ```json
8
+ * { "post": { "id": "1", "title": "Hello" } }
9
+ * { "posts": [{ "id": "1", … }, { "id": "2", … }] }
10
+ * ```
11
+ *
12
+ * Any additional keys in the payload are treated as sideloaded (compound)
13
+ * data. `meta` and `links` keys are reserved and forwarded to the document.
14
+ *
15
+ * Sideloaded types are inferred from the payload key via `modelNameFromPayloadKey`
16
+ * (default: singularization).
17
+ *
18
+ * Extends `JsonSerializer` so it inherits flat `normalize` and `serialize`
19
+ * behaviour and only overrides document-level parsing via `_buildDocument`.
20
+ */
21
+
22
+ import { injectable } from 'tsyringe';
23
+ import pluralize from 'pluralize';
24
+ import {
25
+ type ModelClassMeta,
26
+ type NormalizeRequestType,
27
+ type NormalizedDocument,
28
+ type NormalizedResource,
29
+ } from './Serializer.js';
30
+ import { JsonSerializer } from './JsonSerializer.js';
31
+
32
+ @injectable()
33
+ export class RestSerializer extends JsonSerializer {
34
+ /**
35
+ * Returns the plural payload key for a model name.
36
+ * e.g. `'post'` → `'posts'`
37
+ */
38
+ payloadKeyFromModelName(modelName: string): string {
39
+ return pluralize.plural(modelName);
40
+ }
41
+
42
+ /**
43
+ * Returns the model name for a payload root key.
44
+ * e.g. `'posts'` → `'post'`
45
+ */
46
+ modelNameFromPayloadKey(key: string): string {
47
+ return pluralize.singular(key);
48
+ }
49
+
50
+ /**
51
+ * Builds a `NormalizedDocument` from a root-key REST payload.
52
+ *
53
+ * - Looks for the model data under the singular or plural root key.
54
+ * - Treats every other non-reserved key as a sideloaded type.
55
+ * - Preserves `meta` and `links` from the root.
56
+ */
57
+ protected override _buildDocument(
58
+ store: unknown,
59
+ modelClass: ModelClassMeta,
60
+ payload: unknown,
61
+ _id: string | null,
62
+ _requestType: NormalizeRequestType,
63
+ ): NormalizedDocument {
64
+ if (payload === null || payload === undefined || typeof payload !== 'object') {
65
+ return { data: null };
66
+ }
67
+ const hash = payload as Record<string, unknown>;
68
+ const singular = modelClass.modelName;
69
+ const plural = this.payloadKeyFromModelName(singular);
70
+
71
+ let data: NormalizedResource | NormalizedResource[] | null = null;
72
+ const primaryKeys = new Set<string>();
73
+
74
+ if (singular in hash) {
75
+ primaryKeys.add(singular);
76
+ const raw = hash[singular];
77
+ if (Array.isArray(raw)) {
78
+ data = raw
79
+ .map((entry) => this.normalize(store, modelClass, entry))
80
+ .filter((resource): resource is NormalizedResource => resource !== null);
81
+ } else {
82
+ data = this.normalize(store, modelClass, raw);
83
+ }
84
+ } else if (plural in hash) {
85
+ primaryKeys.add(plural);
86
+ const raw = hash[plural];
87
+ if (Array.isArray(raw)) {
88
+ data = raw
89
+ .map((entry) => this.normalize(store, modelClass, entry))
90
+ .filter((resource): resource is NormalizedResource => resource !== null);
91
+ } else {
92
+ data = this.normalize(store, modelClass, raw);
93
+ }
94
+ }
95
+
96
+ // Collect sideloaded records from all remaining root keys.
97
+ const included: NormalizedResource[] = [];
98
+ for (const [key, value] of Object.entries(hash)) {
99
+ if (primaryKeys.has(key)) {
100
+ continue;
101
+ }
102
+ if (key === 'meta' || key === 'links') {
103
+ continue;
104
+ }
105
+ const sideloadType = this.modelNameFromPayloadKey(key);
106
+ const sideloadClass: ModelClassMeta = {
107
+ modelName: sideloadType,
108
+ attributes: new Map(),
109
+ relationships: new Map(),
110
+ };
111
+ if (Array.isArray(value)) {
112
+ for (const item of value) {
113
+ const normalized = this.normalize(store, sideloadClass, item);
114
+ if (normalized) {
115
+ included.push(normalized);
116
+ }
117
+ }
118
+ } else if (value && typeof value === 'object') {
119
+ const normalized = this.normalize(store, sideloadClass, value);
120
+ if (normalized) {
121
+ included.push(normalized);
122
+ }
123
+ }
124
+ }
125
+
126
+ const doc: NormalizedDocument = { data };
127
+ if (included.length > 0) {
128
+ doc.included = included;
129
+ }
130
+ if (hash.meta && typeof hash.meta === 'object') {
131
+ doc.meta = hash.meta as Record<string, unknown>;
132
+ }
133
+ if (hash.links && typeof hash.links === 'object') {
134
+ doc.links = hash.links as Record<string, string>;
135
+ }
136
+ return doc;
137
+ }
138
+ }