@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.
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/CacheHandler-BTU_rYkv.js +208 -0
- package/dist/CacheHandler-BTU_rYkv.js.map +1 -0
- package/dist/CacheHandler-CXgY9IJo.cjs +2 -0
- package/dist/CacheHandler-CXgY9IJo.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +2 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js +261 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +1 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js +194 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js.map +1 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs +2 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs.map +1 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js +123 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js.map +1 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs +2 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs.map +1 -0
- package/dist/ODataAdapter-C4IHK4BK.js +157 -0
- package/dist/ODataAdapter-C4IHK4BK.js.map +1 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs +2 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs.map +1 -0
- package/dist/RestAdapter-B4aRvs4m.js +355 -0
- package/dist/RestAdapter-B4aRvs4m.js.map +1 -0
- package/dist/RestAdapter-CJOwTsKK.cjs +2 -0
- package/dist/RestAdapter-CJOwTsKK.cjs.map +1 -0
- package/dist/SchemaService-DZwkFgZu.js +102 -0
- package/dist/SchemaService-DZwkFgZu.js.map +1 -0
- package/dist/SchemaService-Di_yjVzU.cjs +2 -0
- package/dist/SchemaService-Di_yjVzU.cjs.map +1 -0
- package/dist/Serializer-95gi5edy.cjs +2 -0
- package/dist/Serializer-95gi5edy.cjs.map +1 -0
- package/dist/Serializer-FxJbsZ50.js +139 -0
- package/dist/Serializer-FxJbsZ50.js.map +1 -0
- package/dist/Store-BdwMrbDi.cjs +2 -0
- package/dist/Store-BdwMrbDi.cjs.map +1 -0
- package/dist/Store-CZ7Z-Nme.js +912 -0
- package/dist/Store-CZ7Z-Nme.js.map +1 -0
- package/dist/adapter/Adapter.d.ts +146 -0
- package/dist/adapter/Adapter.d.ts.map +1 -0
- package/dist/adapter/MemoryAdapter.d.ts +44 -0
- package/dist/adapter/MemoryAdapter.d.ts.map +1 -0
- package/dist/adapter/RestAdapter.d.ts +57 -0
- package/dist/adapter/RestAdapter.d.ts.map +1 -0
- package/dist/adapter/index.cjs +2 -0
- package/dist/adapter/index.cjs.map +1 -0
- package/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +8 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/date-Bj4O2W1F.js +107 -0
- package/dist/date-Bj4O2W1F.js.map +1 -0
- package/dist/date-CRCe-9gf.cjs +2 -0
- package/dist/date-CRCe-9gf.cjs.map +1 -0
- package/dist/decorators-HQ1KnRdh.cjs +2 -0
- package/dist/decorators-HQ1KnRdh.cjs.map +1 -0
- package/dist/decorators-Zr35qr6A.js +50 -0
- package/dist/decorators-Zr35qr6A.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/json-api/JsonApiAdapter.d.ts +38 -0
- package/dist/json-api/JsonApiAdapter.d.ts.map +1 -0
- package/dist/json-api/JsonApiSerializer.d.ts +73 -0
- package/dist/json-api/JsonApiSerializer.d.ts.map +1 -0
- package/dist/json-api/index.cjs +2 -0
- package/dist/json-api/index.cjs.map +1 -0
- package/dist/json-api/index.d.ts +3 -0
- package/dist/json-api/index.d.ts.map +1 -0
- package/dist/json-api/index.js +6 -0
- package/dist/json-api/index.js.map +1 -0
- package/dist/model/Errors.d.ts +46 -0
- package/dist/model/Errors.d.ts.map +1 -0
- package/dist/model/Model.d.ts +226 -0
- package/dist/model/Model.d.ts.map +1 -0
- package/dist/model/Snapshot.d.ts +72 -0
- package/dist/model/Snapshot.d.ts.map +1 -0
- package/dist/model/StateMachine.d.ts +45 -0
- package/dist/model/StateMachine.d.ts.map +1 -0
- package/dist/model/index.cjs +2 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.ts +6 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +11 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/relationships.d.ts +182 -0
- package/dist/model/relationships.d.ts.map +1 -0
- package/dist/odata/ODataAdapter.d.ts +67 -0
- package/dist/odata/ODataAdapter.d.ts.map +1 -0
- package/dist/odata/index.cjs +2 -0
- package/dist/odata/index.cjs.map +1 -0
- package/dist/odata/index.d.ts +2 -0
- package/dist/odata/index.d.ts.map +1 -0
- package/dist/odata/index.js +5 -0
- package/dist/odata/index.js.map +1 -0
- package/dist/relationships-B55LBaCW.cjs +2 -0
- package/dist/relationships-B55LBaCW.cjs.map +1 -0
- package/dist/relationships-BEXANmWg.js +821 -0
- package/dist/relationships-BEXANmWg.js.map +1 -0
- package/dist/request/CacheHandler.d.ts +50 -0
- package/dist/request/CacheHandler.d.ts.map +1 -0
- package/dist/request/FetchHandler.d.ts +41 -0
- package/dist/request/FetchHandler.d.ts.map +1 -0
- package/dist/request/RequestManager.d.ts +52 -0
- package/dist/request/RequestManager.d.ts.map +1 -0
- package/dist/request/index.cjs +2 -0
- package/dist/request/index.cjs.map +1 -0
- package/dist/request/index.d.ts +5 -0
- package/dist/request/index.d.ts.map +1 -0
- package/dist/request/index.js +7 -0
- package/dist/request/index.js.map +1 -0
- package/dist/request/types.d.ts +111 -0
- package/dist/request/types.d.ts.map +1 -0
- package/dist/schema/SchemaService.d.ts +58 -0
- package/dist/schema/SchemaService.d.ts.map +1 -0
- package/dist/schema/decorators.d.ts +50 -0
- package/dist/schema/decorators.d.ts.map +1 -0
- package/dist/schema/index.cjs +2 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/types.d.ts +61 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts +80 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts.map +1 -0
- package/dist/serializer/JsonSerializer.d.ts +52 -0
- package/dist/serializer/JsonSerializer.d.ts.map +1 -0
- package/dist/serializer/RestSerializer.d.ts +43 -0
- package/dist/serializer/RestSerializer.d.ts.map +1 -0
- package/dist/serializer/Serializer.d.ts +202 -0
- package/dist/serializer/Serializer.d.ts.map +1 -0
- package/dist/serializer/index.cjs +2 -0
- package/dist/serializer/index.cjs.map +1 -0
- package/dist/serializer/index.d.ts +5 -0
- package/dist/serializer/index.d.ts.map +1 -0
- package/dist/serializer/index.js +9 -0
- package/dist/serializer/index.js.map +1 -0
- package/dist/store/IdentityMap.d.ts +53 -0
- package/dist/store/IdentityMap.d.ts.map +1 -0
- package/dist/store/RecordArray.d.ts +114 -0
- package/dist/store/RecordArray.d.ts.map +1 -0
- package/dist/store/Store.d.ts +395 -0
- package/dist/store/Store.d.ts.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +8 -0
- package/dist/store/index.js.map +1 -0
- package/dist/transforms/Transform.d.ts +49 -0
- package/dist/transforms/Transform.d.ts.map +1 -0
- package/dist/transforms/boolean.d.ts +26 -0
- package/dist/transforms/boolean.d.ts.map +1 -0
- package/dist/transforms/date.d.ts +22 -0
- package/dist/transforms/date.d.ts.map +1 -0
- package/dist/transforms/index.cjs +2 -0
- package/dist/transforms/index.cjs.map +1 -0
- package/dist/transforms/index.d.ts +6 -0
- package/dist/transforms/index.d.ts.map +1 -0
- package/dist/transforms/index.js +9 -0
- package/dist/transforms/index.js.map +1 -0
- package/dist/transforms/number.d.ts +17 -0
- package/dist/transforms/number.d.ts.map +1 -0
- package/dist/transforms/string.d.ts +18 -0
- package/dist/transforms/string.d.ts.map +1 -0
- package/dist/types-C9NB2gRj.js +7 -0
- package/dist/types-C9NB2gRj.js.map +1 -0
- package/dist/types-uWOXMPWW.cjs +2 -0
- package/dist/types-uWOXMPWW.cjs.map +1 -0
- package/package.json +140 -0
- package/src/adapter/Adapter.ts +320 -0
- package/src/adapter/MemoryAdapter.ts +216 -0
- package/src/adapter/RestAdapter.ts +248 -0
- package/src/adapter/index.ts +7 -0
- package/src/index.ts +17 -0
- package/src/json-api/JsonApiAdapter.ts +93 -0
- package/src/json-api/JsonApiSerializer.ts +245 -0
- package/src/json-api/index.ts +2 -0
- package/src/model/Errors.ts +100 -0
- package/src/model/Model.ts +683 -0
- package/src/model/Snapshot.ts +162 -0
- package/src/model/StateMachine.ts +149 -0
- package/src/model/index.ts +20 -0
- package/src/model/relationships.ts +484 -0
- package/src/odata/ODataAdapter.ts +245 -0
- package/src/odata/index.ts +1 -0
- package/src/request/CacheHandler.ts +125 -0
- package/src/request/FetchHandler.ts +119 -0
- package/src/request/RequestManager.ts +112 -0
- package/src/request/index.ts +4 -0
- package/src/request/types.ts +139 -0
- package/src/schema/SchemaService.ts +161 -0
- package/src/schema/decorators.ts +162 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/types.ts +66 -0
- package/src/serializer/EmbeddedRecordsMixin.ts +257 -0
- package/src/serializer/JsonSerializer.ts +173 -0
- package/src/serializer/RestSerializer.ts +138 -0
- package/src/serializer/Serializer.ts +397 -0
- package/src/serializer/index.ts +15 -0
- package/src/store/IdentityMap.ts +110 -0
- package/src/store/RecordArray.ts +210 -0
- package/src/store/Store.ts +1391 -0
- package/src/store/index.ts +11 -0
- package/src/transforms/Transform.ts +52 -0
- package/src/transforms/boolean.ts +57 -0
- package/src/transforms/date.ts +48 -0
- package/src/transforms/index.ts +5 -0
- package/src/transforms/number.ts +42 -0
- package/src/transforms/string.ts +35 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializer that implements the [JSON:API](https://jsonapi.org) specification.
|
|
3
|
+
*
|
|
4
|
+
* ## Normalization
|
|
5
|
+
*
|
|
6
|
+
* Parses a JSON:API compound document `{ data, included, meta, links }` into
|
|
7
|
+
* a `NormalizedDocument`.
|
|
8
|
+
*
|
|
9
|
+
* For each resource object:
|
|
10
|
+
* - `type` is converted from JSON:API plural form to model name via
|
|
11
|
+
* `modelNameFromPayloadKey` (singularization).
|
|
12
|
+
* - `id` is coerced to a string.
|
|
13
|
+
* - `attributes` are copied directly.
|
|
14
|
+
* - `relationships` data refs (`{ type, id }`) are normalized the same way.
|
|
15
|
+
*
|
|
16
|
+
* Side-loaded records in `included` are normalized individually, each using
|
|
17
|
+
* its own `type` as the model class placeholder.
|
|
18
|
+
*
|
|
19
|
+
* ## Serialization
|
|
20
|
+
*
|
|
21
|
+
* Produces a JSON:API resource object wrapped in `{ data: … }`:
|
|
22
|
+
*
|
|
23
|
+
* ```json
|
|
24
|
+
* {
|
|
25
|
+
* "data": {
|
|
26
|
+
* "type": "posts",
|
|
27
|
+
* "attributes": { "title": "Hello" },
|
|
28
|
+
* "relationships": {
|
|
29
|
+
* "author": { "data": { "type": "users", "id": "1" } }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* `id` is included when `options.includeId` is `true`.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { injectable } from 'tsyringe';
|
|
39
|
+
import pluralize from 'pluralize';
|
|
40
|
+
import {
|
|
41
|
+
Serializer,
|
|
42
|
+
type ModelClassMeta,
|
|
43
|
+
type NormalizeRequestType,
|
|
44
|
+
type NormalizedDocument,
|
|
45
|
+
type NormalizedResource,
|
|
46
|
+
type SerializerSnapshot,
|
|
47
|
+
} from '@mobx-data/serializer';
|
|
48
|
+
|
|
49
|
+
/** Internal shape of a raw JSON:API resource object. */
|
|
50
|
+
interface JsonApiResource {
|
|
51
|
+
type: string;
|
|
52
|
+
id?: string | number | null;
|
|
53
|
+
attributes?: Record<string, unknown>;
|
|
54
|
+
relationships?: Record<string, {
|
|
55
|
+
data:
|
|
56
|
+
| { type: string; id: string | number }
|
|
57
|
+
| Array<{ type: string; id: string | number }>
|
|
58
|
+
| null;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@injectable()
|
|
63
|
+
export class JsonApiSerializer extends Serializer {
|
|
64
|
+
/**
|
|
65
|
+
* Returns the plural JSON:API `type` string for a model name.
|
|
66
|
+
* e.g. `'post'` → `'posts'`
|
|
67
|
+
*/
|
|
68
|
+
payloadKeyFromModelName(modelName: string): string {
|
|
69
|
+
return pluralize.plural(modelName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the model name for a JSON:API `type` string.
|
|
74
|
+
* e.g. `'posts'` → `'post'`
|
|
75
|
+
*/
|
|
76
|
+
modelNameFromPayloadKey(key: string): string {
|
|
77
|
+
return pluralize.singular(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes a single JSON:API resource object into a `NormalizedResource`.
|
|
82
|
+
* Returns `null` for absent, non-object, or type-less payloads.
|
|
83
|
+
*/
|
|
84
|
+
override normalize(
|
|
85
|
+
_store: unknown,
|
|
86
|
+
_modelClass: ModelClassMeta,
|
|
87
|
+
payload: unknown,
|
|
88
|
+
_prop?: string,
|
|
89
|
+
): NormalizedResource | null {
|
|
90
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const resource = payload as JsonApiResource;
|
|
94
|
+
if (!resource.type) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const normalized: NormalizedResource = {
|
|
98
|
+
type: this.modelNameFromPayloadKey(resource.type),
|
|
99
|
+
id: resource.id === null || resource.id === undefined ? null : String(resource.id),
|
|
100
|
+
};
|
|
101
|
+
if (resource.attributes) {
|
|
102
|
+
normalized.attributes = { ...resource.attributes };
|
|
103
|
+
}
|
|
104
|
+
if (resource.relationships) {
|
|
105
|
+
const relationships: NonNullable<NormalizedResource['relationships']> = {};
|
|
106
|
+
for (const [name, rel] of Object.entries(resource.relationships)) {
|
|
107
|
+
if (rel.data === null) {
|
|
108
|
+
relationships[name] = { data: null };
|
|
109
|
+
} else if (Array.isArray(rel.data)) {
|
|
110
|
+
relationships[name] = {
|
|
111
|
+
data: rel.data.map((ref) => ({
|
|
112
|
+
type: this.modelNameFromPayloadKey(ref.type),
|
|
113
|
+
id: String(ref.id),
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
relationships[name] = {
|
|
118
|
+
data: {
|
|
119
|
+
type: this.modelNameFromPayloadKey(rel.data.type),
|
|
120
|
+
id: String(rel.data.id),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
normalized.relationships = relationships;
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Normalizes a full JSON:API compound document.
|
|
132
|
+
*
|
|
133
|
+
* - `data` (single or array) → primary resources
|
|
134
|
+
* - `included` → side-loaded resources pushed to `normalizedDoc.included`
|
|
135
|
+
* - `meta` and `links` → forwarded as-is
|
|
136
|
+
*/
|
|
137
|
+
override normalizeResponse(
|
|
138
|
+
store: unknown,
|
|
139
|
+
modelClass: ModelClassMeta,
|
|
140
|
+
payload: unknown,
|
|
141
|
+
_id: string | null,
|
|
142
|
+
_requestType: NormalizeRequestType,
|
|
143
|
+
): NormalizedDocument {
|
|
144
|
+
if (!payload || typeof payload !== 'object') {
|
|
145
|
+
return { data: null };
|
|
146
|
+
}
|
|
147
|
+
const doc = payload as {
|
|
148
|
+
data?: JsonApiResource | JsonApiResource[] | null;
|
|
149
|
+
included?: JsonApiResource[];
|
|
150
|
+
meta?: Record<string, unknown>;
|
|
151
|
+
links?: Record<string, string>;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let data: NormalizedResource | NormalizedResource[] | null = null;
|
|
155
|
+
if (doc.data === null || doc.data === undefined) {
|
|
156
|
+
data = null;
|
|
157
|
+
} else if (Array.isArray(doc.data)) {
|
|
158
|
+
data = doc.data
|
|
159
|
+
.map((resource) => this.normalize(store, modelClass, resource))
|
|
160
|
+
.filter((resource): resource is NormalizedResource => resource !== null);
|
|
161
|
+
} else {
|
|
162
|
+
data = this.normalize(store, modelClass, doc.data);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const normalizedDoc: NormalizedDocument = { data };
|
|
166
|
+
if (doc.included && doc.included.length > 0) {
|
|
167
|
+
const included: NormalizedResource[] = [];
|
|
168
|
+
for (const resource of doc.included) {
|
|
169
|
+
const placeholder: ModelClassMeta = {
|
|
170
|
+
modelName: this.modelNameFromPayloadKey(resource.type),
|
|
171
|
+
attributes: new Map(),
|
|
172
|
+
relationships: new Map(),
|
|
173
|
+
};
|
|
174
|
+
const normalized = this.normalize(store, placeholder, resource);
|
|
175
|
+
if (normalized) {
|
|
176
|
+
included.push(normalized);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
normalizedDoc.included = included;
|
|
180
|
+
}
|
|
181
|
+
if (doc.meta) {
|
|
182
|
+
normalizedDoc.meta = doc.meta;
|
|
183
|
+
}
|
|
184
|
+
if (doc.links) {
|
|
185
|
+
normalizedDoc.links = doc.links;
|
|
186
|
+
}
|
|
187
|
+
return normalizedDoc;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Serializes a record snapshot to a JSON:API `{ data: … }` document.
|
|
192
|
+
*
|
|
193
|
+
* Attributes are placed in `data.attributes`; relationships are placed in
|
|
194
|
+
* `data.relationships` with proper `{ data: { type, id } }` structure.
|
|
195
|
+
* `id` is included in `data` when `options.includeId` is `true`.
|
|
196
|
+
*/
|
|
197
|
+
override serialize(
|
|
198
|
+
snapshot: SerializerSnapshot,
|
|
199
|
+
options?: { includeId?: boolean },
|
|
200
|
+
): Record<string, unknown> {
|
|
201
|
+
const attributes: Record<string, unknown> = {};
|
|
202
|
+
snapshot.eachAttribute((key) => {
|
|
203
|
+
attributes[this.keyForAttribute(key)] = snapshot.attr(key);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const relationships: Record<string, { data: unknown }> = {};
|
|
207
|
+
snapshot.eachRelationship((_key, rel) => {
|
|
208
|
+
const { name } = rel;
|
|
209
|
+
const payloadKey = this.keyForRelationship(name);
|
|
210
|
+
const payloadType = this.payloadKeyFromModelName(rel.type);
|
|
211
|
+
if (rel.kind === 'belongsTo') {
|
|
212
|
+
const target = snapshot.belongsTo(name) as
|
|
213
|
+
| { id: string; type?: string }
|
|
214
|
+
| null
|
|
215
|
+
| undefined;
|
|
216
|
+
relationships[payloadKey] = target && target.id != null
|
|
217
|
+
? { data: { type: payloadType, id: String(target.id) } }
|
|
218
|
+
: { data: null };
|
|
219
|
+
} else {
|
|
220
|
+
const targets = (snapshot.hasMany(name) as Array<{
|
|
221
|
+
id: string; type?: string;
|
|
222
|
+
}> | null) ?? [];
|
|
223
|
+
relationships[payloadKey] = {
|
|
224
|
+
data: targets.map((target) => ({
|
|
225
|
+
type: payloadType,
|
|
226
|
+
id: String(target.id),
|
|
227
|
+
})),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const data: Record<string, unknown> = {
|
|
233
|
+
type: this.payloadKeyFromModelName(snapshot.modelName),
|
|
234
|
+
attributes,
|
|
235
|
+
};
|
|
236
|
+
if (options?.includeId && snapshot.id !== null) {
|
|
237
|
+
data.id = snapshot.id;
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(relationships).length > 0) {
|
|
240
|
+
data.relationships = relationships;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { data };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observable collection of validation error messages keyed by attribute name.
|
|
3
|
+
*
|
|
4
|
+
* `Errors` is attached to every `Model` instance as `record.errors`. It
|
|
5
|
+
* mirrors Ember Data's `DS.Errors` API: each attribute can hold multiple
|
|
6
|
+
* `ErrorMessage` objects, and the map is MobX-observable so templates / computed
|
|
7
|
+
* properties that read `isEmpty` or `length` react automatically when errors
|
|
8
|
+
* are added or cleared.
|
|
9
|
+
*
|
|
10
|
+
* Typical lifecycle:
|
|
11
|
+
* 1. Server returns a 422; the serializer calls `store.errors.add(…)`.
|
|
12
|
+
* 2. The UI reads `record.errors.get('email')` to display messages.
|
|
13
|
+
* 3. The user corrects the field; `record.errors.remove('email')` clears it.
|
|
14
|
+
* 4. A successful save calls `record.errors.clear()` to wipe all messages.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { injectable } from 'tsyringe';
|
|
18
|
+
import {
|
|
19
|
+
makeObservable, observable, computed, action,
|
|
20
|
+
} from 'mobx';
|
|
21
|
+
|
|
22
|
+
/** A single validation error for one attribute. */
|
|
23
|
+
export interface ErrorMessage {
|
|
24
|
+
/** Attribute name the error belongs to. */
|
|
25
|
+
attribute: string;
|
|
26
|
+
/** Human-readable error message. */
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@injectable()
|
|
31
|
+
export class Errors implements Iterable<[string, ErrorMessage[]]> {
|
|
32
|
+
private _errors: Map<string, ErrorMessage[]> = new Map();
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
makeObservable<this, '_errors'>(this, {
|
|
36
|
+
_errors: observable.shallow,
|
|
37
|
+
isEmpty: computed,
|
|
38
|
+
length: computed,
|
|
39
|
+
add: action,
|
|
40
|
+
remove: action,
|
|
41
|
+
clear: action,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `true` when there are no validation errors. */
|
|
46
|
+
get isEmpty(): boolean {
|
|
47
|
+
return this._errors.size === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Total number of error messages across all attributes. */
|
|
51
|
+
get length(): number {
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const messages of this._errors.values()) {
|
|
54
|
+
total += messages.length;
|
|
55
|
+
}
|
|
56
|
+
return total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns all error messages for `attribute`, or an empty array. */
|
|
60
|
+
get(attribute: string): ErrorMessage[] {
|
|
61
|
+
return this._errors.get(attribute) ?? [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Returns `true` when `attribute` has at least one error message. */
|
|
65
|
+
has(attribute: string): boolean {
|
|
66
|
+
const messages = this._errors.get(attribute);
|
|
67
|
+
return !!messages && messages.length > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Appends one or more error messages for `attribute`.
|
|
72
|
+
* Existing messages are preserved — this is an additive operation.
|
|
73
|
+
*/
|
|
74
|
+
add(attribute: string, message: string | string[]): void {
|
|
75
|
+
const incoming = Array.isArray(message) ? message : [message];
|
|
76
|
+
const existing = this._errors.get(attribute) ?? [];
|
|
77
|
+
const next: ErrorMessage[] = [
|
|
78
|
+
...existing,
|
|
79
|
+
...incoming.map((msg) => ({ attribute, message: msg })),
|
|
80
|
+
];
|
|
81
|
+
this._errors.set(attribute, next);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Removes all error messages for `attribute`. */
|
|
85
|
+
remove(attribute: string): void {
|
|
86
|
+
this._errors.delete(attribute);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Removes all error messages for every attribute. */
|
|
90
|
+
clear(): void {
|
|
91
|
+
this._errors.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Iterates `[attributeName, ErrorMessage[]]` pairs. */
|
|
95
|
+
* [Symbol.iterator](): Iterator<[string, ErrorMessage[]]> {
|
|
96
|
+
for (const entry of this._errors.entries()) {
|
|
97
|
+
yield entry;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|