mobx-keystone-mindreframer
Version:
A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more
128 lines (109 loc) • 4.08 kB
text/typescript
import { action, set } from "mobx"
import type { O } from "ts-toolbelt"
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
import type { ModelCreationData } from "../modelShared/BaseModelShared"
import { modelInfoByClass } from "../modelShared/modelInfo"
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
import { applyModelInitializers } from "../modelShared/newModel"
import { noDefaultValue } from "../modelShared/prop"
import { tweakModel } from "../tweaker/tweakModel"
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
import { failure, inDevMode, makePropReadonly } from "../utils"
import type { AnyModel } from "./BaseModel"
import { getModelIdPropertyName, getModelMetadata } from "./getModelMetadata"
import { modelTypeKey } from "./metadata"
import type { ModelConstructorOptions } from "./ModelConstructorOptions"
import { assertIsModelClass } from "./utils"
/**
* @ignore
* @internal
*/
export const internalNewModel = action(
"newModel",
<M extends AnyModel>(
origModelObj: M,
initialData: ModelCreationData<M> | undefined,
options: Pick<ModelConstructorOptions, "modelClass" | "snapshotInitialData" | "generateNewIds">
): M => {
const { modelClass: _modelClass, snapshotInitialData, generateNewIds } = options
const modelClass = _modelClass!
if (inDevMode()) {
assertIsModelClass(modelClass, "modelClass")
}
const modelObj = origModelObj as O.Writable<M>
const modelInfo = modelInfoByClass.get(modelClass)
if (!modelInfo) {
throw failure(
`no model info for class ${modelClass.name} could be found - did you forget to add the @model decorator?`
)
}
const modelIdPropertyName = getModelIdPropertyName(modelClass)
const modelProps = getInternalModelClassPropsInfo(modelClass)
const modelIdPropData = modelProps[modelIdPropertyName]!
let id
if (snapshotInitialData) {
let sn = snapshotInitialData.unprocessedSnapshot
if (generateNewIds) {
id = (modelIdPropData.defaultFn as () => string)()
} else {
id = sn[modelIdPropertyName]
}
if (modelObj.fromSnapshot) {
sn = modelObj.fromSnapshot(sn)
}
initialData = snapshotInitialData.snapshotToInitialData(sn)
} else {
// use symbol if provided
if (initialData![modelIdPropertyName]) {
id = initialData![modelIdPropertyName]
} else {
id = (modelIdPropData.defaultFn as () => string)()
}
}
modelObj[modelTypeKey] = modelInfo.name
// fill in defaults in initial data
const modelPropsKeys = Object.keys(modelProps)
for (let i = 0; i < modelPropsKeys.length; i++) {
const k = modelPropsKeys[i]
// id is already initialized above
if (k !== modelIdPropertyName) {
const v = (initialData as any)[k]
if (v === undefined || v === null) {
let newValue: any = v
const propData = modelProps[k]
if (propData.defaultFn !== noDefaultValue) {
newValue = propData.defaultFn()
} else if (propData.defaultValue !== noDefaultValue) {
newValue = propData.defaultValue
}
set(initialData as any, k, newValue)
}
}
}
set(initialData as any, modelIdPropertyName, id)
tweakModel(modelObj, undefined)
// create observable data object with initial data
let obsData = tweakPlainObject(
initialData!,
{ parent: modelObj, path: "$" },
modelObj[modelTypeKey],
false,
true
)
// link it, and make it readonly
modelObj.$ = obsData
if (inDevMode()) {
makePropReadonly(modelObj, "$", true)
}
// type check it if needed
if (isModelAutoTypeCheckingEnabled() && getModelMetadata(modelClass).dataType) {
const err = modelObj.typeCheck()
if (err) {
err.throw(modelObj)
}
}
// run any extra initializers for the class as needed
applyModelInitializers(modelClass, modelObj)
return modelObj as M
}
)