mobx-keystone 2.3.1 → 2.4.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/CHANGELOG.md +22 -0
- package/dist/mobx-keystone.esm.js +3604 -2414
- package/dist/mobx-keystone.esm.mjs +3604 -2414
- package/dist/mobx-keystone.umd.js +3685 -2481
- package/dist/types/action/CallKind.d.ts +29 -0
- package/dist/types/action/applyAction.d.ts +7 -1
- package/dist/types/action/index.d.ts +1 -0
- package/dist/types/action/resolveCallName.d.ts +1 -0
- package/dist/types/actionMiddlewares/actionSerialization/actionSerialization.d.ts +14 -0
- package/dist/types/actionMiddlewares/actionSerialization/applySerializedAction.d.ts +6 -0
- package/dist/types/actionMiddlewares/actionSerialization/core.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/patch/emitPatch.d.ts +6 -0
- package/dist/types/serialization/Call.d.ts +55 -0
- package/dist/types/serialization/CallError.d.ts +96 -0
- package/dist/types/serialization/JsonValue.d.ts +6 -0
- package/dist/types/serialization/Serializer.d.ts +98 -0
- package/dist/types/serialization/ValueSerializer.d.ts +93 -0
- package/dist/types/serialization/applyCall.d.ts +54 -0
- package/dist/types/serialization/builtinSerializers.d.ts +53 -0
- package/dist/types/serialization/codec.d.ts +1 -0
- package/dist/types/serialization/index.d.ts +8 -0
- package/dist/types/serialization/references.d.ts +74 -0
- package/dist/types/serialization/resolvedCall.d.ts +1 -0
- package/package.json +1 -1
- package/src/action/CallKind.ts +29 -0
- package/src/action/applyAction.ts +22 -49
- package/src/action/applySet.ts +21 -1
- package/src/action/index.ts +1 -0
- package/src/action/resolveCallName.ts +207 -0
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +14 -0
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +6 -0
- package/src/actionMiddlewares/actionSerialization/core.ts +7 -0
- package/src/actionMiddlewares/actionSerialization/index.ts +2 -0
- package/src/actionMiddlewares/actionSerialization/objectPathSerializer.ts +3 -2
- package/src/index.ts +1 -0
- package/src/parent/path.ts +45 -12
- package/src/patch/emitPatch.ts +6 -0
- package/src/serialization/Call.ts +58 -0
- package/src/serialization/CallError.ts +153 -0
- package/src/serialization/JsonValue.ts +12 -0
- package/src/serialization/Serializer.ts +342 -0
- package/src/serialization/ValueSerializer.ts +104 -0
- package/src/serialization/applyCall.ts +190 -0
- package/src/serialization/builtinSerializers.ts +264 -0
- package/src/serialization/codec.ts +360 -0
- package/src/serialization/index.ts +15 -0
- package/src/serialization/references.ts +225 -0
- package/src/serialization/resolvedCall.ts +169 -0
- package/src/standardActions/actions.ts +11 -3
- package/src/utils/errorDiagnostics.ts +9 -4
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { frozenKey } from "../frozen/Frozen"
|
|
2
|
+
import { modelTypeKey } from "../model/metadata"
|
|
3
|
+
import type { PathElement } from "../parent/pathTypes"
|
|
4
|
+
import { copyOwnEnumerableProps, hasOwnProp, isArray, isObject, isPlainObject } from "../utils"
|
|
5
|
+
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
6
|
+
import { builtinEncodedTypeof, snapshotTag } from "./builtinSerializers"
|
|
7
|
+
import { badPayloadError, CallError, CallErrorCode } from "./CallError"
|
|
8
|
+
import type { JsonValue } from "./JsonValue"
|
|
9
|
+
import type { DecodeContext, EncodeContext, ValueSerializer } from "./ValueSerializer"
|
|
10
|
+
import { serializationMarkerKey } from "./ValueSerializer"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
*
|
|
15
|
+
* The tag of an escaped plain object: `{ "$mobxKeystone": "object", "v": { ...fields } }`.
|
|
16
|
+
*/
|
|
17
|
+
export const escapedObjectTag = "object"
|
|
18
|
+
|
|
19
|
+
const reservedKeys: readonly string[] = [serializationMarkerKey, modelTypeKey, frozenKey]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export interface SerializerConfig {
|
|
25
|
+
/**
|
|
26
|
+
* Custom serializers followed by the built-ins (if any).
|
|
27
|
+
*/
|
|
28
|
+
readonly serializers: readonly ValueSerializer[]
|
|
29
|
+
/**
|
|
30
|
+
* The serializers that could encode a value, by its `typeof`, or `"plain"` for plain arrays and
|
|
31
|
+
* objects, which no built-in encodes (custom ones are in every list).
|
|
32
|
+
*/
|
|
33
|
+
readonly byTypeof: ReadonlyMap<SerializerGroup, readonly ValueSerializer[]>
|
|
34
|
+
readonly byTag: ReadonlyMap<string, ValueSerializer>
|
|
35
|
+
/**
|
|
36
|
+
* The serializer that decodes values with `$modelType` / `$frozen` (the one tagged
|
|
37
|
+
* `snapshot`), if in the list.
|
|
38
|
+
*/
|
|
39
|
+
readonly unmarked: ValueSerializer | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const typeofResults = [
|
|
43
|
+
"string",
|
|
44
|
+
"number",
|
|
45
|
+
"bigint",
|
|
46
|
+
"boolean",
|
|
47
|
+
"symbol",
|
|
48
|
+
"undefined",
|
|
49
|
+
"object",
|
|
50
|
+
"function",
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export type TypeofResult = (typeof typeofResults)[number]
|
|
57
|
+
|
|
58
|
+
type SerializerGroup = TypeofResult | "plain"
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
*
|
|
63
|
+
* Groups serializers by the `typeof` of the values they could encode, plus `"plain"` for plain
|
|
64
|
+
* arrays and objects, keeping their order.
|
|
65
|
+
*/
|
|
66
|
+
export function groupSerializersByTypeof(
|
|
67
|
+
serializers: readonly ValueSerializer[]
|
|
68
|
+
): ReadonlyMap<SerializerGroup, readonly ValueSerializer[]> {
|
|
69
|
+
const byTypeof = new Map<SerializerGroup, readonly ValueSerializer[]>(
|
|
70
|
+
typeofResults.map((t) => [
|
|
71
|
+
t,
|
|
72
|
+
serializers.filter((s) => {
|
|
73
|
+
const encodedTypeof = builtinEncodedTypeof.get(s)
|
|
74
|
+
return encodedTypeof === undefined || encodedTypeof === t
|
|
75
|
+
}),
|
|
76
|
+
])
|
|
77
|
+
)
|
|
78
|
+
byTypeof.set(
|
|
79
|
+
"plain",
|
|
80
|
+
serializers.filter((s) => !builtinEncodedTypeof.has(s))
|
|
81
|
+
)
|
|
82
|
+
return byTypeof
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type PlainKind = "array" | "object" | undefined
|
|
86
|
+
|
|
87
|
+
function getPlainKind(value: unknown): PlainKind {
|
|
88
|
+
if (!isObject(value)) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
if (isArray(value)) {
|
|
92
|
+
return "array"
|
|
93
|
+
}
|
|
94
|
+
return isPlainObject(value) ? "object" : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Runs `fn` with `key` pushed to the error path (without allocating a closure per value).
|
|
99
|
+
*/
|
|
100
|
+
function withPathSegment<A, R>(key: PathElement, fn: (arg: A) => R, arg: A): R {
|
|
101
|
+
const diagnostics = getCurrentErrorDiagnosticsContext()
|
|
102
|
+
if (!diagnostics) {
|
|
103
|
+
return fn(arg)
|
|
104
|
+
}
|
|
105
|
+
diagnostics.pushPath(key)
|
|
106
|
+
try {
|
|
107
|
+
return fn(arg)
|
|
108
|
+
} finally {
|
|
109
|
+
diagnostics.popPath()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export function isUnmarkedSerializer(serializer: ValueSerializer): boolean {
|
|
117
|
+
return serializer.tag === snapshotTag
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isSnapshotJson(payload: unknown): boolean {
|
|
121
|
+
return (
|
|
122
|
+
isPlainObject(payload) && (hasOwnProp(payload, modelTypeKey) || hasOwnProp(payload, frozenKey))
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
export function markValue(tag: string, payload: JsonValue | undefined): JsonValue {
|
|
130
|
+
return payload === undefined
|
|
131
|
+
? { [serializationMarkerKey]: tag }
|
|
132
|
+
: { [serializationMarkerKey]: tag, v: payload }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
export function wrapSerializerError(
|
|
139
|
+
error: unknown,
|
|
140
|
+
code: CallErrorCode,
|
|
141
|
+
tag: string,
|
|
142
|
+
value: unknown
|
|
143
|
+
): CallError {
|
|
144
|
+
if (error instanceof CallError) {
|
|
145
|
+
return error
|
|
146
|
+
}
|
|
147
|
+
const op = code === CallErrorCode.CannotEncode ? "encode" : "decode"
|
|
148
|
+
return new CallError({
|
|
149
|
+
code,
|
|
150
|
+
message: `the '${tag}' serializer failed to ${op} the value`,
|
|
151
|
+
previewValue: value,
|
|
152
|
+
cause: error,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function describeValue(value: unknown): string {
|
|
157
|
+
switch (typeof value) {
|
|
158
|
+
case "function":
|
|
159
|
+
return "a function"
|
|
160
|
+
case "symbol":
|
|
161
|
+
return "a symbol"
|
|
162
|
+
case "object": {
|
|
163
|
+
const proto = value === null ? null : Object.getPrototypeOf(value)
|
|
164
|
+
const name = proto?.constructor?.name
|
|
165
|
+
return name ? `an instance of ${name}` : "an object"
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
return String(value)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @internal
|
|
174
|
+
*
|
|
175
|
+
* Creates the encoding context for one encoding: `ctx.encode` encodes any value, going through
|
|
176
|
+
* the list of serializers. It must run inside an error diagnostics context.
|
|
177
|
+
*/
|
|
178
|
+
export function createEncodeContext(config: SerializerConfig, root: object): EncodeContext {
|
|
179
|
+
// values being encoded, to detect cycles
|
|
180
|
+
const ancestors = new Set<unknown>()
|
|
181
|
+
|
|
182
|
+
const encodeWithSerializer = (serializer: ValueSerializer, value: unknown): JsonValue => {
|
|
183
|
+
let payload: JsonValue | undefined
|
|
184
|
+
try {
|
|
185
|
+
payload = serializer.encode(value, ctx)
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw wrapSerializerError(e, CallErrorCode.CannotEncode, serializer.tag, value)
|
|
188
|
+
}
|
|
189
|
+
if (!isUnmarkedSerializer(serializer)) {
|
|
190
|
+
return markValue(serializer.tag, payload)
|
|
191
|
+
}
|
|
192
|
+
// written unmarked, so it must mark itself to be decoded by the same serializer
|
|
193
|
+
if (!isSnapshotJson(payload)) {
|
|
194
|
+
throw new CallError({
|
|
195
|
+
code: CallErrorCode.CannotEncode,
|
|
196
|
+
message: `the '${snapshotTag}' serializer must encode a snapshot (with $modelType or $frozen)`,
|
|
197
|
+
previewValue: value,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
return payload!
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const encodePlain = (value: unknown, plainKind: PlainKind): JsonValue => {
|
|
204
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
205
|
+
return value
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
208
|
+
return value
|
|
209
|
+
}
|
|
210
|
+
if (plainKind === "array") {
|
|
211
|
+
const array = value as readonly unknown[]
|
|
212
|
+
const len = array.length
|
|
213
|
+
const out: JsonValue[] = new Array(len)
|
|
214
|
+
for (let i = 0; i < len; i++) {
|
|
215
|
+
out[i] = ctx.encode(array[i], i)
|
|
216
|
+
}
|
|
217
|
+
return out
|
|
218
|
+
}
|
|
219
|
+
if (plainKind === "object") {
|
|
220
|
+
const object = value as Record<string, unknown>
|
|
221
|
+
const out = copyOwnEnumerableProps({} as Record<string, JsonValue>, object, ctx.encode)
|
|
222
|
+
return reservedKeys.some((k) => hasOwnProp(object, k))
|
|
223
|
+
? markValue(escapedObjectTag, out)
|
|
224
|
+
: out
|
|
225
|
+
}
|
|
226
|
+
throw new CallError({
|
|
227
|
+
code: CallErrorCode.CannotEncode,
|
|
228
|
+
message: `cannot encode ${describeValue(value)}: no serializer handles it`,
|
|
229
|
+
previewValue: value,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const encodeAny = (value: unknown): JsonValue => {
|
|
234
|
+
const plainKind = getPlainKind(value)
|
|
235
|
+
const serializers = config.byTypeof.get(plainKind ? "plain" : typeof value)!
|
|
236
|
+
const len = serializers.length
|
|
237
|
+
for (let i = 0; i < len; i++) {
|
|
238
|
+
const serializer = serializers[i]
|
|
239
|
+
let claimed: boolean
|
|
240
|
+
try {
|
|
241
|
+
claimed = serializer.is(value, ctx)
|
|
242
|
+
} catch (e) {
|
|
243
|
+
throw wrapSerializerError(e, CallErrorCode.CannotEncode, serializer.tag, value)
|
|
244
|
+
}
|
|
245
|
+
if (claimed) {
|
|
246
|
+
return encodeWithSerializer(serializer, value)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return encodePlain(value, plainKind)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const encodeChecked = (value: unknown): JsonValue => {
|
|
253
|
+
if (!isObject(value) && typeof value !== "function") {
|
|
254
|
+
return encodeAny(value)
|
|
255
|
+
}
|
|
256
|
+
if (ancestors.has(value)) {
|
|
257
|
+
throw new CallError({
|
|
258
|
+
code: CallErrorCode.CannotEncode,
|
|
259
|
+
message: "cannot encode a value that contains itself",
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
ancestors.add(value)
|
|
263
|
+
try {
|
|
264
|
+
return encodeAny(value)
|
|
265
|
+
} finally {
|
|
266
|
+
ancestors.delete(value)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ctx: EncodeContext = {
|
|
271
|
+
root,
|
|
272
|
+
encode(value: unknown, key?: PathElement): JsonValue {
|
|
273
|
+
return key === undefined ? encodeChecked(value) : withPathSegment(key, encodeChecked, value)
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return ctx
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @internal
|
|
282
|
+
*
|
|
283
|
+
* Creates the decoding context for one decoding: `ctx.decode` decodes any JSON value. It must run
|
|
284
|
+
* inside an error diagnostics context.
|
|
285
|
+
*/
|
|
286
|
+
export function createDecodeContext(
|
|
287
|
+
config: SerializerConfig,
|
|
288
|
+
root: object,
|
|
289
|
+
generateNewIds: boolean
|
|
290
|
+
): DecodeContext {
|
|
291
|
+
const decodeWithSerializer = (serializer: ValueSerializer, json: unknown): unknown => {
|
|
292
|
+
try {
|
|
293
|
+
return serializer.decode(json, ctx)
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw wrapSerializerError(e, CallErrorCode.BadArgument, serializer.tag, json)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const decodeFields = (json: Record<string, unknown>): Record<string, unknown> =>
|
|
300
|
+
copyOwnEnumerableProps({} as Record<string, unknown>, json, (v, k) =>
|
|
301
|
+
ctx.decode(v as JsonValue, k)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const decodeMarked = (json: Record<string, unknown>): unknown => {
|
|
305
|
+
const tag = json[serializationMarkerKey]
|
|
306
|
+
if (tag === escapedObjectTag) {
|
|
307
|
+
if (!isPlainObject(json.v)) {
|
|
308
|
+
throw badPayloadError(escapedObjectTag, json)
|
|
309
|
+
}
|
|
310
|
+
return decodeFields(json.v)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const serializer = typeof tag === "string" ? config.byTag.get(tag) : undefined
|
|
314
|
+
if (!serializer) {
|
|
315
|
+
throw new CallError({
|
|
316
|
+
code: CallErrorCode.BadArgument,
|
|
317
|
+
message: `no serializer handles the tag ${JSON.stringify(tag)}`,
|
|
318
|
+
previewValue: json,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
return decodeWithSerializer(serializer, json.v)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const decodeAny = (json: unknown): unknown => {
|
|
325
|
+
if (
|
|
326
|
+
json === null ||
|
|
327
|
+
typeof json === "string" ||
|
|
328
|
+
typeof json === "number" ||
|
|
329
|
+
typeof json === "boolean"
|
|
330
|
+
) {
|
|
331
|
+
return json
|
|
332
|
+
}
|
|
333
|
+
if (isArray(json)) {
|
|
334
|
+
return json.map((v, i) => ctx.decode(v, i))
|
|
335
|
+
}
|
|
336
|
+
if (isPlainObject(json)) {
|
|
337
|
+
if (hasOwnProp(json, serializationMarkerKey)) {
|
|
338
|
+
return decodeMarked(json)
|
|
339
|
+
}
|
|
340
|
+
if (config.unmarked && (hasOwnProp(json, modelTypeKey) || hasOwnProp(json, frozenKey))) {
|
|
341
|
+
return decodeWithSerializer(config.unmarked, json)
|
|
342
|
+
}
|
|
343
|
+
return decodeFields(json)
|
|
344
|
+
}
|
|
345
|
+
throw new CallError({
|
|
346
|
+
code: CallErrorCode.BadArgument,
|
|
347
|
+
message: `cannot decode ${describeValue(json)}: it is not a JSON value`,
|
|
348
|
+
previewValue: json,
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const ctx: DecodeContext = {
|
|
353
|
+
root,
|
|
354
|
+
generateNewIds,
|
|
355
|
+
decode: (json: JsonValue, key?: PathElement): unknown =>
|
|
356
|
+
key === undefined ? decodeAny(json) : withPathSegment(key, decodeAny, json),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return ctx
|
|
360
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./applyCall"
|
|
2
|
+
export { builtinSerializers } from "./builtinSerializers"
|
|
3
|
+
export * from "./Call"
|
|
4
|
+
export { CallError, CallErrorCode, type CallErrorData, type CallErrorJson } from "./CallError"
|
|
5
|
+
export * from "./JsonValue"
|
|
6
|
+
export {
|
|
7
|
+
type IdRefJson,
|
|
8
|
+
idRefs,
|
|
9
|
+
type PathRefJson,
|
|
10
|
+
pathRefs,
|
|
11
|
+
type ReferenceNodeInfo,
|
|
12
|
+
type ReferenceSerializerOptions,
|
|
13
|
+
} from "./references"
|
|
14
|
+
export * from "./Serializer"
|
|
15
|
+
export * from "./ValueSerializer"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { rootPathToTargetPathIds } from "../actionMiddlewares/utils"
|
|
2
|
+
import type { AnyModel } from "../model/BaseModel"
|
|
3
|
+
import { getModelMetadata } from "../model/getModelMetadata"
|
|
4
|
+
import { modelTypeKey } from "../model/metadata"
|
|
5
|
+
import { isModel } from "../model/utils"
|
|
6
|
+
import {
|
|
7
|
+
fastGetRoot,
|
|
8
|
+
fastGetRootPath,
|
|
9
|
+
isPathElement,
|
|
10
|
+
resolvePathCheckingIds,
|
|
11
|
+
skipIdChecking,
|
|
12
|
+
} from "../parent/path"
|
|
13
|
+
import type { Path, PathElement } from "../parent/pathTypes"
|
|
14
|
+
import { getModelRefId, resolveId } from "../ref/core"
|
|
15
|
+
import { isTreeNode } from "../tweaker/core"
|
|
16
|
+
import { isArray, isPlainObject } from "../utils"
|
|
17
|
+
import { badPayloadError, CallError, CallErrorCode } from "./CallError"
|
|
18
|
+
import type { ValueSerializer } from "./ValueSerializer"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a reference serializer's `when` option is told about a node.
|
|
22
|
+
*/
|
|
23
|
+
export interface ReferenceNodeInfo {
|
|
24
|
+
/**
|
|
25
|
+
* Whether the node is a model.
|
|
26
|
+
*/
|
|
27
|
+
readonly isModel: boolean
|
|
28
|
+
/**
|
|
29
|
+
* Whether the node is a value-type model (`Model({...}, { valueType: true })`).
|
|
30
|
+
*/
|
|
31
|
+
readonly isValueType: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options of the built-in reference serializers.
|
|
36
|
+
*/
|
|
37
|
+
export interface ReferenceSerializerOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Decides, for every node the serializer could reference, whether it does. A node it declines
|
|
40
|
+
* falls through the list like any other value: a model ends up as its snapshot, a tree array
|
|
41
|
+
* or plain object is walked. The call's target ignores it: it is always a reference.
|
|
42
|
+
*
|
|
43
|
+
* Defaults to declining value-type models: `(node, { isValueType }) => !isValueType`.
|
|
44
|
+
*/
|
|
45
|
+
when?(node: object, info: ReferenceNodeInfo): boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A path reference, the payload of `{ "$mobxKeystone": "pathRef", "v": ... }`.
|
|
50
|
+
*/
|
|
51
|
+
export type PathRefJson = {
|
|
52
|
+
/**
|
|
53
|
+
* Path from the root to the node, through data nodes (as in patches).
|
|
54
|
+
*/
|
|
55
|
+
path: PathElement[]
|
|
56
|
+
/**
|
|
57
|
+
* Id of the model reached by each step of the path (`null` for non-models).
|
|
58
|
+
* Optional when decoding: without it, ids aren't checked.
|
|
59
|
+
*/
|
|
60
|
+
ids?: (string | null)[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An id reference, the payload of `{ "$mobxKeystone": "idRef", "v": ... }`.
|
|
65
|
+
*/
|
|
66
|
+
export type IdRefJson = {
|
|
67
|
+
/**
|
|
68
|
+
* The model type (`$modelType`).
|
|
69
|
+
*/
|
|
70
|
+
type: string
|
|
71
|
+
/**
|
|
72
|
+
* The model id.
|
|
73
|
+
*/
|
|
74
|
+
id: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const defaultWhen = (_node: object, info: ReferenceNodeInfo) => !info.isValueType
|
|
78
|
+
|
|
79
|
+
function getNodeInfo(node: object): ReferenceNodeInfo {
|
|
80
|
+
const nodeIsModel = isModel(node)
|
|
81
|
+
return {
|
|
82
|
+
isModel: nodeIsModel,
|
|
83
|
+
isValueType: nodeIsModel && getModelMetadata(node).valueType,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export function isNodeOfTree(value: unknown, root: object): value is object {
|
|
91
|
+
return isTreeNode(value) && fastGetRoot(value, false) === root
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export const pathRefTag = "pathRef"
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
*
|
|
102
|
+
* Encodes a node of the tree as a full path reference.
|
|
103
|
+
*/
|
|
104
|
+
export function encodePathRef(node: object): PathRefJson {
|
|
105
|
+
const rootPath = fastGetRootPath(node, false)
|
|
106
|
+
return {
|
|
107
|
+
path: [...rootPath.path],
|
|
108
|
+
ids: rootPathToTargetPathIds(rootPath),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @internal
|
|
114
|
+
*
|
|
115
|
+
* Decodes a path reference against a tree.
|
|
116
|
+
*/
|
|
117
|
+
export function decodePathRef(json: unknown, root: object): object {
|
|
118
|
+
if (!isPlainObject(json) || !isArray(json.path) || !json.path.every(isPathElement)) {
|
|
119
|
+
throw badPayloadError(pathRefTag, json)
|
|
120
|
+
}
|
|
121
|
+
const path = json.path as Path
|
|
122
|
+
const ids = json.ids
|
|
123
|
+
if (
|
|
124
|
+
ids !== undefined &&
|
|
125
|
+
(!isArray(ids) ||
|
|
126
|
+
ids.length !== path.length ||
|
|
127
|
+
!ids.every((id) => id === null || typeof id === "string"))
|
|
128
|
+
) {
|
|
129
|
+
throw badPayloadError(pathRefTag, json)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = resolvePathCheckingIds(
|
|
133
|
+
root,
|
|
134
|
+
path,
|
|
135
|
+
(ids as (string | null)[] | undefined) ?? path.map(() => skipIdChecking)
|
|
136
|
+
)
|
|
137
|
+
// only tree nodes can be referenced, so anything else is not a valid reference
|
|
138
|
+
if (!result.resolved || !isTreeNode(result.value)) {
|
|
139
|
+
throw new CallError({
|
|
140
|
+
code: CallErrorCode.NotFound,
|
|
141
|
+
message: `object at path ${JSON.stringify(path)}${
|
|
142
|
+
ids === undefined ? "" : ` with ids ${JSON.stringify(ids)}`
|
|
143
|
+
} could not be resolved`,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return result.value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* References nodes of the tree by their path from the root, checking the ids of the models along
|
|
151
|
+
* it: `{ "$mobxKeystone": "pathRef", "v": { "path": ["cards", 2], "ids": [null, "c1"] } }`.
|
|
152
|
+
* It reaches any node, including tree arrays and plain objects.
|
|
153
|
+
*
|
|
154
|
+
* @param options Options.
|
|
155
|
+
* @returns The reference serializer.
|
|
156
|
+
*/
|
|
157
|
+
export function pathRefs(
|
|
158
|
+
options?: ReferenceSerializerOptions
|
|
159
|
+
): ValueSerializer<object, PathRefJson> {
|
|
160
|
+
const when = options?.when ?? defaultWhen
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
tag: pathRefTag,
|
|
164
|
+
|
|
165
|
+
is(value, ctx): value is object {
|
|
166
|
+
return isNodeOfTree(value, ctx.root) && when(value, getNodeInfo(value))
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
encode: encodePathRef,
|
|
170
|
+
|
|
171
|
+
decode(json, ctx) {
|
|
172
|
+
return decodePathRef(json, ctx.root)
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
claimsTarget(value, ctx) {
|
|
176
|
+
return isNodeOfTree(value, ctx.root)
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function claimsById(value: unknown, root: object): value is AnyModel {
|
|
182
|
+
return isModel(value) && getModelRefId(value) !== undefined && isNodeOfTree(value, root)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* References models of the tree that have an id by their type and id:
|
|
187
|
+
* `{ "$mobxKeystone": "idRef", "v": { "type": "app/Card", "id": "c1" } }`. Ids are read and
|
|
188
|
+
* resolved as `getModelRefId` and `resolveId` do, and the type must match.
|
|
189
|
+
*
|
|
190
|
+
* @param options Options.
|
|
191
|
+
* @returns The reference serializer.
|
|
192
|
+
*/
|
|
193
|
+
export function idRefs(options?: ReferenceSerializerOptions): ValueSerializer<AnyModel, IdRefJson> {
|
|
194
|
+
const when = options?.when ?? defaultWhen
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
tag: "idRef",
|
|
198
|
+
|
|
199
|
+
is(value, ctx): value is AnyModel {
|
|
200
|
+
return claimsById(value, ctx.root) && when(value, getNodeInfo(value))
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
encode(value) {
|
|
204
|
+
return { type: value[modelTypeKey], id: getModelRefId(value)! }
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
decode(json, ctx) {
|
|
208
|
+
if (!isPlainObject(json) || typeof json.type !== "string" || typeof json.id !== "string") {
|
|
209
|
+
throw badPayloadError("idRef", json)
|
|
210
|
+
}
|
|
211
|
+
const found = resolveId(ctx.root, json.id)
|
|
212
|
+
if (!isModel(found) || found[modelTypeKey] !== json.type) {
|
|
213
|
+
throw new CallError({
|
|
214
|
+
code: CallErrorCode.NotFound,
|
|
215
|
+
message: `model of type '${json.type}' with id '${json.id}' could not be resolved`,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
return found
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
claimsTarget(value, ctx) {
|
|
222
|
+
return claimsById(value, ctx.root)
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
}
|