-
Notifications
You must be signed in to change notification settings - Fork 25
/
BaseModel.ts
332 lines (290 loc) · 9.81 KB
/
BaseModel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import { observable } from "mobx"
import { O } from "ts-toolbelt"
import { getGlobalConfig } from "../globalConfig"
import { memoTransformCache } from "../propTransform/propTransform"
import { getSnapshot } from "../snapshot/getSnapshot"
import { SnapshotInOfModel, SnapshotInOfObject, SnapshotOutOfModel } from "../snapshot/SnapshotOf"
import { typesModel } from "../typeChecking/model"
import { typeCheck } from "../typeChecking/typeCheck"
import { TypeCheckError } from "../typeChecking/TypeCheckError"
import { assertIsObject } from "../utils"
import { modelIdKey, modelTypeKey } from "./metadata"
import { ModelConstructorOptions } from "./ModelConstructorOptions"
import { modelInfoByClass } from "./modelInfo"
import { internalNewModel } from "./newModel"
import { assertIsModelClass } from "./utils"
/**
* @ignore
*/
export const propsDataTypeSymbol = Symbol()
/**
* @ignore
*/
export const propsCreationDataTypeSymbol = Symbol()
/**
* @ignore
*/
export const instanceDataTypeSymbol = Symbol()
/**
* @ignore
*/
export const instanceCreationDataTypeSymbol = Symbol()
/**
* @ignore
* @internal
*/
export const modelInitializedSymbol = Symbol("modelInitialized")
/**
* Base abstract class for models. Use `Model` instead when extending.
*
* Never override the constructor, use `onInit` or `onAttachedToRootStore` instead.
*
* @typeparam Data Data type.
* @typeparam CreationData Creation data type.
*/
export abstract class BaseModel<
PropsData extends { [k: string]: any },
PropsCreationData extends { [k: string]: any },
InstanceData extends { [k: string]: any } = PropsData,
InstanceCreationData extends { [k: string]: any } = PropsCreationData
> {
// just to make typing work properly
[propsDataTypeSymbol]: PropsData;
[propsCreationDataTypeSymbol]: PropsCreationData;
[instanceDataTypeSymbol]: InstanceData;
[instanceCreationDataTypeSymbol]: InstanceCreationData;
/**
* Model type name.
*/
readonly [modelTypeKey]: string;
/**
* Model internal id. Can be modified inside a model action.
*/
[modelIdKey]: string
/**
* Can be overriden to offer a reference id to be used in reference resolution.
* By default it will use `$modelId`.
*/
getRefId(): string {
return this[modelIdKey]
}
/**
* Called after the model has been created.
*/
onInit?(): void
/**
* Data part of the model, which is observable and will be serialized in snapshots.
* Use it if one of the data properties matches one of the model properties/functions.
* This also allows access to the backed values of transformed properties.
*/
readonly $!: PropsData
/**
* Optional hook that will run once this model instance is attached to the tree of a model marked as
* root store via `registerRootStore`.
* Basically this is the place where you know the full root store is complete and where things such as
* middlewares, effects (reactions, etc), and other side effects should be registered, since it means
* that the model is now part of the active application state.
*
* It can return a disposer that will be run once this model instance is detached from such root store tree.
*
* @param rootStore
* @returns
*/
onAttachedToRootStore?(rootStore: object): (() => void) | void
/**
* Optional transformation that will be run when converting from a snapshot to the data part of the model.
* Useful for example to do versioning and keep the data part up to date with the latest version of the model.
*
* @param snapshot The custom input snapshot.
* @returns An input snapshot that must match the current model input snapshot.
*/
fromSnapshot?(snapshot: {
[k: string]: any
}): SnapshotInOfObject<PropsCreationData> & { [modelTypeKey]?: string; [modelIdKey]?: string }
/**
* Performs a type check over the model instance.
* For this to work a data type has to be declared in the model decorator.
*
* @returns A `TypeCheckError` or `null` if there is no error.
*/
typeCheck(): TypeCheckError | null {
const type = typesModel<this>(this.constructor as any)
return typeCheck(type, this as any)
}
/**
* Creates an instance of Model.
*/
constructor(data: InstanceCreationData & { [modelIdKey]?: string }) {
let initialData = data as any
const {
snapshotInitialData,
modelClass,
propsWithTransforms,
generateNewIds,
}: ModelConstructorOptions = arguments[1] as any
Object.setPrototypeOf(this, modelClass!.prototype)
const self = this as any
// let's always use the one from the prototype
delete self[modelIdKey]
// delete unnecessary props
delete self[propsDataTypeSymbol]
delete self[propsCreationDataTypeSymbol]
delete self[instanceDataTypeSymbol]
delete self[instanceCreationDataTypeSymbol]
if (!snapshotInitialData) {
// plain new
assertIsObject(initialData, "initialData")
// apply transforms to initial data if needed
const propsWithTransformsLen = propsWithTransforms!.length
if (propsWithTransformsLen > 0) {
initialData = Object.assign(initialData)
for (let i = 0; i < propsWithTransformsLen; i++) {
const propWithTransform = propsWithTransforms![i]
const propName = propWithTransform[0]
const propTransform = propWithTransform[1]
const memoTransform = memoTransformCache.getOrCreateMemoTransform(
this,
propName,
propTransform
)
initialData[propName] = memoTransform.dataToProp(initialData[propName])
}
}
internalNewModel(this, observable.object(initialData, undefined, { deep: false }), {
modelClass,
generateNewIds: true,
})
} else {
// from snapshot
internalNewModel(this, undefined, { modelClass, snapshotInitialData, generateNewIds })
}
}
toString(options?: { withData?: boolean }) {
const finalOptions = {
withData: true,
...options,
}
const firstPart = `${this.constructor.name}#${this[modelTypeKey]}`
return finalOptions.withData
? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]`
: `[${firstPart}]`
}
}
// these props will never be hoisted to this (except for model id)
/**
* @internal
*/
export const baseModelPropNames = new Set<keyof AnyModel>([
modelTypeKey,
modelIdKey,
"onInit",
"$",
"getRefId",
"onAttachedToRootStore",
"fromSnapshot",
"typeCheck",
])
/**
* Any kind of model instance.
*/
export interface AnyModel extends BaseModel<any, any> {}
/**
* Extracts the instance type of a model class.
*/
export interface ModelClass<M extends AnyModel> {
new (initialData: any): M
}
/**
* A model class declaration, made of a base model and the model interface.
*/
export type ModelClassDeclaration<BaseModelClass, ModelInterface> = BaseModelClass & {
new (...args: any[]): ModelInterface
}
/**
* @deprecated Should not be needed anymore.
*
* Tricks Typescript into accepting abstract classes as a parameter for `ExtendedModel`.
* Does nothing in runtime.
*
* @typeparam T Abstract model class type.
* @param type Abstract model class.
* @returns
*/
export function abstractModelClass<T>(type: T): T & Object {
return type as any
}
/**
* Tricks Typescript into accepting a particular kind of generic class as a parameter for `ExtendedModel`.
* Does nothing in runtime.
*
* @typeparam T Generic model class type.
* @param type Generic model class.
* @returns
*/
export function modelClass<T extends AnyModel>(type: { prototype: T }): ModelClass<T> {
return type as any
}
/**
* The props data type of a model.
*/
export type ModelPropsData<M extends AnyModel> = M["$"]
/**
* The props creation data type of a model.
*/
export type ModelPropsCreationData<M extends AnyModel> = M[typeof propsCreationDataTypeSymbol]
/**
* The instance data type of a model.
*/
export type ModelInstanceData<M extends AnyModel> = M[typeof instanceDataTypeSymbol]
/**
* The transformed creation data type of a model.
*/
export type ModelInstanceCreationData<M extends AnyModel> = M[typeof instanceCreationDataTypeSymbol]
/**
* Add missing model metadata to a model creation snapshot to generate a proper model snapshot.
* Usually used alongside `fromSnapshot`.
*
* @typeparam M Model type.
* @param modelClass Model class.
* @param snapshot Model creation snapshot without metadata.
* @param [internalId] Model internal ID, or `undefined` to generate a new one.
* @returns The model snapshot (including metadata).
*/
export function modelSnapshotInWithMetadata<M extends AnyModel>(
modelClass: ModelClass<M>,
snapshot: O.Omit<SnapshotInOfModel<M>, typeof modelIdKey | typeof modelTypeKey>,
internalId: string = getGlobalConfig().modelIdGenerator()
): SnapshotInOfModel<M> {
assertIsModelClass(modelClass, "modelClass")
assertIsObject(snapshot, "initialData")
const modelInfo = modelInfoByClass.get(modelClass)!
return {
...snapshot,
[modelTypeKey]: modelInfo.name,
[modelIdKey]: internalId,
} as any
}
/**
* Add missing model metadata to a model output snapshot to generate a proper model snapshot.
* Usually used alongside `applySnapshot`.
*
* @typeparam M Model type.
* @param modelClass Model class.
* @param snapshot Model output snapshot without metadata.
* @param [internalId] Model internal ID, or `undefined` to generate a new one.
* @returns The model snapshot (including metadata).
*/
export function modelSnapshotOutWithMetadata<M extends AnyModel>(
modelClass: ModelClass<M>,
snapshot: O.Omit<SnapshotOutOfModel<M>, typeof modelIdKey | typeof modelTypeKey>,
internalId: string = getGlobalConfig().modelIdGenerator()
): SnapshotOutOfModel<M> {
assertIsModelClass(modelClass, "modelClass")
assertIsObject(snapshot, "initialData")
const modelInfo = modelInfoByClass.get(modelClass)!
return {
...snapshot,
[modelTypeKey]: modelInfo.name,
[modelIdKey]: internalId,
} as any
}