@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,216 @@
1
+ /**
2
+ * In-memory adapter for testing and server-side rendering.
3
+ *
4
+ * `MemoryAdapter` stores records in plain `Map` structures with no network I/O.
5
+ * It implements the full adapter interface (findRecord, findAll, findMany, query,
6
+ * queryRecord, createRecord, updateRecord, deleteRecord) so it can serve as a
7
+ * drop-in replacement for `RestAdapter` in unit tests or SSR hydration scenarios.
8
+ *
9
+ * Use `seed()` to pre-populate data and `reset()` to clear all state between
10
+ * test cases.
11
+ */
12
+
13
+ import { injectable } from 'tsyringe';
14
+ import { Adapter, type AdapterSnapshot } from './Adapter.js';
15
+
16
+ interface MemoryRecord {
17
+ id: string;
18
+ type: string;
19
+ attributes: Record<string, unknown>;
20
+ }
21
+
22
+ @injectable()
23
+ export class MemoryAdapter extends Adapter {
24
+ private storage: Map<string, Map<string, MemoryRecord>> = new Map();
25
+
26
+ private nextId: Map<string, number> = new Map();
27
+
28
+ private getCollection(modelName: string): Map<string, MemoryRecord> {
29
+ let collection = this.storage.get(modelName);
30
+ if (!collection) {
31
+ collection = new Map();
32
+ this.storage.set(modelName, collection);
33
+ }
34
+ return collection;
35
+ }
36
+
37
+ private generateId(modelName: string): string {
38
+ const current = this.nextId.get(modelName) ?? 1;
39
+ this.nextId.set(modelName, current + 1);
40
+ return String(current);
41
+ }
42
+
43
+ /**
44
+ * Pre-populates the adapter with records for a given model type.
45
+ * Auto-increments the internal ID counter to avoid collisions with
46
+ * subsequently created records.
47
+ *
48
+ * @param modelName - The model type to seed.
49
+ * @param records - Array of plain objects; each must have an `id` key.
50
+ */
51
+ seed(modelName: string, records: Array<{ id: string; [key: string]: unknown }>): void {
52
+ const collection = this.getCollection(modelName);
53
+ for (const record of records) {
54
+ const { id, ...attributes } = record;
55
+ collection.set(id, { id, type: modelName, attributes });
56
+ const numId = Number(id);
57
+ if (!Number.isNaN(numId)) {
58
+ const current = this.nextId.get(modelName) ?? 1;
59
+ if (numId >= current) {
60
+ this.nextId.set(modelName, numId + 1);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ /** Clears all stored records and resets ID counters. */
67
+ reset(): void {
68
+ this.storage.clear();
69
+ this.nextId.clear();
70
+ }
71
+
72
+ override async findRecord(
73
+ _store: unknown,
74
+ modelName: string,
75
+ id: string,
76
+ ): Promise<unknown> {
77
+ const collection = this.getCollection(modelName);
78
+ const record = collection.get(id);
79
+ if (!record) {
80
+ throw Object.assign(
81
+ new Error(`Record not found: ${modelName}:${id}`),
82
+ { status: 404 },
83
+ );
84
+ }
85
+ return { data: { id: record.id, type: record.type, attributes: { ...record.attributes } } };
86
+ }
87
+
88
+ override async findAll(
89
+ _store: unknown,
90
+ modelName: string,
91
+ ): Promise<unknown> {
92
+ const collection = this.getCollection(modelName);
93
+ const data = Array.from(collection.values()).map((record) => ({
94
+ id: record.id,
95
+ type: record.type,
96
+ attributes: { ...record.attributes },
97
+ }));
98
+ return { data };
99
+ }
100
+
101
+ override async findMany(
102
+ _store: unknown,
103
+ modelName: string,
104
+ ids: string[],
105
+ ): Promise<unknown> {
106
+ const collection = this.getCollection(modelName);
107
+ const data = ids
108
+ .map((id) => collection.get(id))
109
+ .filter((record): record is MemoryRecord => record !== undefined)
110
+ .map((record) => ({
111
+ id: record.id,
112
+ type: record.type,
113
+ attributes: { ...record.attributes },
114
+ }));
115
+ return { data };
116
+ }
117
+
118
+ /** Filters records by exact attribute match on all query keys. */
119
+ override async query(
120
+ _store: unknown,
121
+ modelName: string,
122
+ query: Record<string, unknown>,
123
+ ): Promise<unknown> {
124
+ const collection = this.getCollection(modelName);
125
+ const entries = Array.from(collection.values());
126
+ const filtered = entries.filter((record) => {
127
+ for (const [key, value] of Object.entries(query)) {
128
+ if (record.attributes[key] !== value) {
129
+ return false;
130
+ }
131
+ }
132
+ return true;
133
+ });
134
+ const data = filtered.map((record) => ({
135
+ id: record.id,
136
+ type: record.type,
137
+ attributes: { ...record.attributes },
138
+ }));
139
+ return { data };
140
+ }
141
+
142
+ /** Returns the first record matching the query, or `null`. */
143
+ override async queryRecord(
144
+ _store: unknown,
145
+ modelName: string,
146
+ query: Record<string, unknown>,
147
+ ): Promise<unknown> {
148
+ const result = await this.query(_store, modelName, query) as { data: unknown[] };
149
+ return { data: result.data[0] ?? null };
150
+ }
151
+
152
+ private static safeAssign(
153
+ target: Record<string, unknown>,
154
+ source: Record<string, unknown>,
155
+ ): void {
156
+ for (const [key, value] of Object.entries(source)) {
157
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
158
+ continue;
159
+ }
160
+ target[key] = value;
161
+ }
162
+ }
163
+
164
+ override async createRecord(
165
+ _store: unknown,
166
+ modelName: string,
167
+ snapshot: AdapterSnapshot,
168
+ ): Promise<unknown> {
169
+ const id = this.generateId(modelName);
170
+ const attributes: Record<string, unknown> = {};
171
+ const record = snapshot.record as { _data?: Record<string, unknown> };
172
+ if (record._data) {
173
+ MemoryAdapter.safeAssign(attributes, record._data);
174
+ }
175
+ const entry: MemoryRecord = { id, type: modelName, attributes };
176
+ this.getCollection(modelName).set(id, entry);
177
+ return { data: { id, type: modelName, attributes: { ...attributes } } };
178
+ }
179
+
180
+ override async updateRecord(
181
+ _store: unknown,
182
+ modelName: string,
183
+ snapshot: AdapterSnapshot,
184
+ ): Promise<unknown> {
185
+ const { id } = snapshot;
186
+ if (!id) {
187
+ throw new Error('Cannot update a record without an id');
188
+ }
189
+ const collection = this.getCollection(modelName);
190
+ const existing = collection.get(id);
191
+ if (!existing) {
192
+ throw Object.assign(
193
+ new Error(`Record not found: ${modelName}:${id}`),
194
+ { status: 404 },
195
+ );
196
+ }
197
+ const record = snapshot.record as { _data?: Record<string, unknown> };
198
+ if (record._data) {
199
+ MemoryAdapter.safeAssign(existing.attributes, record._data);
200
+ }
201
+ return { data: { id, type: modelName, attributes: { ...existing.attributes } } };
202
+ }
203
+
204
+ override async deleteRecord(
205
+ _store: unknown,
206
+ modelName: string,
207
+ snapshot: AdapterSnapshot,
208
+ ): Promise<unknown> {
209
+ const { id } = snapshot;
210
+ if (!id) {
211
+ throw new Error('Cannot delete a record without an id');
212
+ }
213
+ this.getCollection(modelName).delete(id);
214
+ return null;
215
+ }
216
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * HTTP/REST adapter that communicates with a JSON REST API.
3
+ *
4
+ * `RestAdapter` is the default concrete adapter. It uses the Fetch API and
5
+ * maps each store operation to a conventional HTTP verb:
6
+ *
7
+ * | Operation | Method |
8
+ * |----------------|--------|
9
+ * | findRecord | GET |
10
+ * | findAll | GET |
11
+ * | findMany | GET |
12
+ * | query | GET |
13
+ * | queryRecord | GET |
14
+ * | createRecord | POST |
15
+ * | updateRecord | PUT |
16
+ * | deleteRecord | DELETE |
17
+ *
18
+ * Subclasses (e.g. `JsonApiAdapter`, `ODataAdapter`) override individual
19
+ * methods to adjust headers, HTTP verbs, or body serialization without
20
+ * reimplementing the full adapter.
21
+ */
22
+
23
+ import { injectable } from 'tsyringe';
24
+ import { Adapter, type AdapterSnapshot } from './Adapter.js';
25
+
26
+ @injectable()
27
+ export class RestAdapter extends Adapter {
28
+ static serializeSnapshotToObject(
29
+ snapshot: AdapterSnapshot,
30
+ ): Record<string, unknown> {
31
+ const body: Record<string, unknown> = {};
32
+ const record = snapshot.record as { _data?: Record<string, unknown> };
33
+ const data = record._data;
34
+ if (data) {
35
+ for (const [key, value] of Object.entries(data)) {
36
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
37
+ continue;
38
+ }
39
+ body[key] = value;
40
+ }
41
+ }
42
+ return body;
43
+ }
44
+
45
+ static toQueryString(query: Record<string, unknown>): string {
46
+ const parts: string[] = [];
47
+ for (const [key, value] of Object.entries(query)) {
48
+ if (value === undefined || value === null) {
49
+ continue;
50
+ }
51
+ parts.push(
52
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
53
+ );
54
+ }
55
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
56
+ }
57
+
58
+ /** Headers sent with every read (GET) request. */
59
+ defaultHeaders(): Record<string, string> {
60
+ return {
61
+ Accept: 'application/json',
62
+ ...this.headers,
63
+ };
64
+ }
65
+
66
+ /** Headers sent with every write (POST / PUT / DELETE) request. */
67
+ mutationHeaders(): Record<string, string> {
68
+ return {
69
+ 'Content-Type': 'application/json',
70
+ ...this.defaultHeaders(),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Low-level fetch wrapper used by all operation methods.
76
+ *
77
+ * - Throws an enriched `Error` (with `status` and `body` properties) for
78
+ * any non-2xx response.
79
+ * - Returns `null` for 204 No Content responses.
80
+ * - Attempts JSON parsing; falls back to the raw text string on failure.
81
+ */
82
+ async _fetchJSON(
83
+ url: string,
84
+ init: RequestInit,
85
+ ): Promise<unknown> {
86
+ const response = await fetch(url, init);
87
+ if (!response.ok) {
88
+ let body: unknown = null;
89
+ try {
90
+ body = await response.json();
91
+ } catch {
92
+ /* ignore */
93
+ }
94
+ const error = Object.assign(
95
+ new Error(`Request failed: ${response.status}`),
96
+ { status: response.status, body },
97
+ );
98
+ throw error;
99
+ }
100
+ if (response.status === 204) {
101
+ return null;
102
+ }
103
+ const text = await response.text();
104
+ if (!text) {
105
+ return null;
106
+ }
107
+ try {
108
+ return JSON.parse(text);
109
+ } catch {
110
+ return text;
111
+ }
112
+ }
113
+
114
+ override async findRecord(
115
+ _store: unknown,
116
+ modelName: string,
117
+ id: string,
118
+ snapshot: AdapterSnapshot,
119
+ options?: { include?: string },
120
+ ): Promise<unknown> {
121
+ let url = this.buildURL(modelName, id, snapshot, 'findRecord');
122
+ if (options?.include) {
123
+ url += `${url.includes('?') ? '&' : '?'}include=${encodeURIComponent(options.include)}`;
124
+ }
125
+ return this._fetchJSON(url, {
126
+ method: 'GET',
127
+ headers: this.defaultHeaders(),
128
+ });
129
+ }
130
+
131
+ override async findAll(
132
+ _store: unknown,
133
+ modelName: string,
134
+ _sinceToken: string | null,
135
+ snapshotArray: AdapterSnapshot[],
136
+ options?: { include?: string },
137
+ ): Promise<unknown> {
138
+ let url = this.buildURL(modelName, null, snapshotArray, 'findAll');
139
+ if (options?.include) {
140
+ url += `${url.includes('?') ? '&' : '?'}include=${encodeURIComponent(options.include)}`;
141
+ }
142
+ return this._fetchJSON(url, {
143
+ method: 'GET',
144
+ headers: this.defaultHeaders(),
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Fetches multiple records by appending an `ids` query parameter.
150
+ * e.g. `/posts?ids=1,2,3`
151
+ */
152
+ override async findMany(
153
+ _store: unknown,
154
+ modelName: string,
155
+ ids: string[],
156
+ snapshots: AdapterSnapshot[],
157
+ ): Promise<unknown> {
158
+ const url = this.buildURL(modelName, ids, snapshots, 'findMany');
159
+ const separator = url.includes('?') ? '&' : '?';
160
+ const fullUrl = `${url}${separator}ids=${ids.map(encodeURIComponent).join(',')}`;
161
+ return this._fetchJSON(fullUrl, {
162
+ method: 'GET',
163
+ headers: this.defaultHeaders(),
164
+ });
165
+ }
166
+
167
+ override async query(
168
+ _store: unknown,
169
+ modelName: string,
170
+ query: Record<string, unknown>,
171
+ ): Promise<unknown> {
172
+ const base = this.buildURL(modelName, null, null, 'query', query);
173
+ const url = `${base}${RestAdapter.toQueryString(query)}`;
174
+ return this._fetchJSON(url, {
175
+ method: 'GET',
176
+ headers: this.defaultHeaders(),
177
+ });
178
+ }
179
+
180
+ override async queryRecord(
181
+ _store: unknown,
182
+ modelName: string,
183
+ query: Record<string, unknown>,
184
+ ): Promise<unknown> {
185
+ const base = this.buildURL(modelName, null, null, 'queryRecord', query);
186
+ const url = `${base}${RestAdapter.toQueryString(query)}`;
187
+ return this._fetchJSON(url, {
188
+ method: 'GET',
189
+ headers: this.defaultHeaders(),
190
+ });
191
+ }
192
+
193
+ override async createRecord(
194
+ _store: unknown,
195
+ modelName: string,
196
+ snapshot: AdapterSnapshot,
197
+ ): Promise<unknown> {
198
+ const url = this.buildURL(modelName, null, snapshot, 'createRecord');
199
+ return this._fetchJSON(url, {
200
+ method: 'POST',
201
+ headers: this.mutationHeaders(),
202
+ body: JSON.stringify(RestAdapter.serializeSnapshotToObject(snapshot)),
203
+ });
204
+ }
205
+
206
+ override async updateRecord(
207
+ _store: unknown,
208
+ modelName: string,
209
+ snapshot: AdapterSnapshot,
210
+ ): Promise<unknown> {
211
+ const url = this.buildURL(modelName, snapshot.id, snapshot, 'updateRecord');
212
+ return this._fetchJSON(url, {
213
+ method: 'PUT',
214
+ headers: this.mutationHeaders(),
215
+ body: JSON.stringify(RestAdapter.serializeSnapshotToObject(snapshot)),
216
+ });
217
+ }
218
+
219
+ override async patchRecord(
220
+ _store: unknown,
221
+ modelName: string,
222
+ snapshot: AdapterSnapshot,
223
+ ): Promise<unknown> {
224
+ const url = this.buildURL(modelName, snapshot.id, snapshot, 'updateRecord');
225
+ const changed = snapshot.changedAttributes();
226
+ const partial: Record<string, unknown> = {};
227
+ for (const [key, [, current]] of Object.entries(changed)) {
228
+ partial[key] = current;
229
+ }
230
+ return this._fetchJSON(url, {
231
+ method: 'PATCH',
232
+ headers: this.mutationHeaders(),
233
+ body: JSON.stringify(partial),
234
+ });
235
+ }
236
+
237
+ override async deleteRecord(
238
+ _store: unknown,
239
+ modelName: string,
240
+ snapshot: AdapterSnapshot,
241
+ ): Promise<unknown> {
242
+ const url = this.buildURL(modelName, snapshot.id, snapshot, 'deleteRecord');
243
+ return this._fetchJSON(url, {
244
+ method: 'DELETE',
245
+ headers: this.defaultHeaders(),
246
+ });
247
+ }
248
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ Adapter,
3
+ type AdapterRequestType,
4
+ type AdapterSnapshot,
5
+ } from './Adapter.js';
6
+ export { RestAdapter } from './RestAdapter.js';
7
+ export { MemoryAdapter } from './MemoryAdapter.js';
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from './schema/index.js';
2
+ export * from './model/index.js';
3
+ export {
4
+ Store,
5
+ type AdapterLike,
6
+ type SerializerLike,
7
+ type FindOptions,
8
+ IdentityMap,
9
+ RecordArray,
10
+ AdapterPopulatedRecordArray,
11
+ } from './store/index.js';
12
+ export * from './adapter/index.js';
13
+ export * from './serializer/index.js';
14
+ export * from './transforms/index.js';
15
+ export * from './request/index.js';
16
+ export * from './json-api/index.js';
17
+ export * from './odata/index.js';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Adapter that implements the [JSON:API](https://jsonapi.org) specification.
3
+ *
4
+ * Extends `RestAdapter` with the following differences:
5
+ *
6
+ * - **MIME types** — `Accept` and `Content-Type` headers are set to
7
+ * `application/vnd.api+json` as required by the spec.
8
+ * - **PATCH for updates** — `updateRecord` uses `PATCH` instead of `PUT`.
9
+ * - **`coalesceFindRequests: true`** — the store coalesces separate
10
+ * `findRecord` calls into a single `findMany` request.
11
+ *
12
+ * URL construction is inherited from `RestAdapter` / `Adapter` unchanged;
13
+ * override `pathForType` or `urlFor*` methods to customize.
14
+ */
15
+
16
+ import { injectable } from 'tsyringe';
17
+ import { RestAdapter, type AdapterSnapshot } from '@mobx-data/adapter';
18
+
19
+ @injectable()
20
+ export class JsonApiAdapter extends RestAdapter {
21
+ /** Always coalesce `findRecord` calls into a single `findMany` request. */
22
+ override coalesceFindRequests = true;
23
+
24
+ /** Returns JSON:API `Accept` header alongside any custom headers. */
25
+ override defaultHeaders(): Record<string, string> {
26
+ return {
27
+ Accept: 'application/vnd.api+json',
28
+ ...this.headers,
29
+ };
30
+ }
31
+
32
+ /** Returns JSON:API `Content-Type` header for mutation requests. */
33
+ override mutationHeaders(): Record<string, string> {
34
+ return {
35
+ 'Content-Type': 'application/vnd.api+json',
36
+ ...this.defaultHeaders(),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Sends a PATCH request to update a record (JSON:API mandates PATCH, not PUT).
42
+ */
43
+ override async updateRecord(
44
+ _store: unknown,
45
+ modelName: string,
46
+ snapshot: AdapterSnapshot,
47
+ ): Promise<unknown> {
48
+ const url = this.buildURL(modelName, snapshot.id, snapshot, 'updateRecord');
49
+ return this._fetchJSON(url, {
50
+ method: 'PATCH',
51
+ headers: this.mutationHeaders(),
52
+ body: JSON.stringify(this._serializeForUpdate(snapshot)),
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Sends a PATCH request with only the changed attributes (partial update).
58
+ * Produces a JSON:API document with only the dirty attribute keys.
59
+ */
60
+ override async patchRecord(
61
+ _store: unknown,
62
+ modelName: string,
63
+ snapshot: AdapterSnapshot,
64
+ ): Promise<unknown> {
65
+ const url = this.buildURL(modelName, snapshot.id, snapshot, 'updateRecord');
66
+ const changed = snapshot.changedAttributes();
67
+ const attributes: Record<string, unknown> = {};
68
+ for (const [key, [, current]] of Object.entries(changed)) {
69
+ attributes[key] = current;
70
+ }
71
+ const body = {
72
+ data: {
73
+ type: modelName,
74
+ id: snapshot.id,
75
+ attributes,
76
+ },
77
+ };
78
+ return this._fetchJSON(url, {
79
+ method: 'PATCH',
80
+ headers: this.mutationHeaders(),
81
+ body: JSON.stringify(body),
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Extracts the raw data object from the snapshot for use as the PATCH body.
87
+ * Subclasses may override this to produce a full JSON:API `{ data: … }` document.
88
+ */
89
+ protected _serializeForUpdate(snapshot: AdapterSnapshot): Record<string, unknown> {
90
+ const rec = snapshot.record as { _data?: Record<string, unknown> };
91
+ return rec._data ? { ...rec._data } : {};
92
+ }
93
+ }