mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
262 lines (217 loc) • 8.17 kB
text/typescript
import { action, set } from "mobx"
import type { O } from "ts-toolbelt"
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
import type { ModelClass, ModelCreationData } from "../modelShared/BaseModelShared"
import { modelInfoByClass } from "../modelShared/modelInfo"
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
import { applyModelInitializers } from "../modelShared/newModel"
import { getModelPropDefaultValue, noDefaultValue } from "../modelShared/prop"
import { Patch } from "../patch/Patch"
import { createPatchForObjectValueChange, emitPatches } from "../patch/emitPatch"
import { tweakModel } from "../tweaker/tweakModel"
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
import { failure, inDevMode, makePropReadonly } from "../utils"
import { setIfDifferent, setIfDifferentWithReturn } from "../utils/setIfDifferent"
import type { AnyModel } from "./BaseModel"
import type { ModelConstructorOptions } from "./ModelConstructorOptions"
import { getModelIdPropertyName, getModelMetadata } from "./getModelMetadata"
import { modelTypeKey } from "./metadata"
import { assertIsModelClass } from "./utils"
/**
* @internal
*/
export const internalNewModel = action(
"newModel",
<M extends AnyModel>(
origModelObj: M,
initialData: ModelCreationData<M>,
modelClass: ModelClass<AnyModel>
): void => {
if (inDevMode) {
assertIsModelClass(modelClass, "modelClass")
}
const { modelInfo, modelIdPropertyName, modelProps, modelIdPropData } =
getModelDetails(modelClass)
// use symbol if provided
if (modelIdPropertyName && modelIdPropData) {
let id: string | undefined
if (initialData[modelIdPropertyName]) {
id = initialData[modelIdPropertyName]
} else {
id = (modelIdPropData._defaultFn as () => string)()
}
setIfDifferent(initialData, modelIdPropertyName, id)
}
const modelObj = origModelObj as O.Writable<M>
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) {
continue
}
const propData = modelProps[k]
const initialValue = initialData[k]
let newValue = initialValue
let changed = false
// apply untransform (if any) if not in snapshot mode
if (propData._transform) {
changed = true
newValue = propData._transform.untransform(newValue, modelObj, k)
}
// apply default value (if needed)
if (newValue == null) {
const defaultValue = getModelPropDefaultValue(propData)
if (defaultValue !== noDefaultValue) {
changed = true
newValue = defaultValue
} else if (!(k in initialData)) {
// for mobx4, we need to set up properties even if they are undefined
changed = true
}
}
if (changed) {
// setIfDifferent not required
set(initialData, k, newValue)
}
}
finalizeNewModel(modelObj, initialData, modelClass)
// type check it if needed
if (isModelAutoTypeCheckingEnabled() && getModelMetadata(modelClass).dataType) {
const err = modelObj.typeCheck()
if (err) {
err.throw()
}
}
}
)
/**
* @internal
*/
export const internalFromSnapshotModel = action(
"fromSnapshotModel",
<M extends AnyModel>(
origModelObj: M,
snapshotInitialData: NonNullable<ModelConstructorOptions["snapshotInitialData"]>,
modelClass: ModelClass<AnyModel>,
generateNewIds: boolean
): void => {
if (inDevMode) {
assertIsModelClass(modelClass, "modelClass")
}
const { modelInfo, modelIdPropertyName, modelProps, modelIdPropData } =
getModelDetails(modelClass)
let id: string | undefined
let sn = snapshotInitialData.unprocessedSnapshot
if (modelIdPropData && modelIdPropertyName) {
if (generateNewIds) {
id = (modelIdPropData._defaultFn as () => string)()
} else {
id = sn[modelIdPropertyName]
}
}
if (modelClass.fromSnapshotProcessor) {
sn = modelClass.fromSnapshotProcessor(sn)
}
const initialData = snapshotInitialData.snapshotToInitialData(sn)
const modelObj = origModelObj as O.Writable<M>
modelObj[modelTypeKey] = modelInfo.name
const patches: Patch[] = []
const inversePatches: Patch[] = []
if (modelIdPropertyName) {
const initialValue = initialData[modelIdPropertyName]
const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)
if (valueChanged) {
const modelIdPath = [modelIdPropertyName]
patches.push(createPatchForObjectValueChange(modelIdPath, initialValue, id))
inversePatches.push(createPatchForObjectValueChange(modelIdPath, id, initialValue))
}
}
// 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) {
continue
}
const propData = modelProps[k]
const initialValue = initialData[k]
let newValue = initialValue
let changed = false
// apply default value (if needed)
if (newValue == null) {
const defaultValue = getModelPropDefaultValue(propData)
if (defaultValue !== noDefaultValue) {
changed = true
newValue = defaultValue
} else if (!(k in initialData!)) {
// for mobx4, we need to set up properties even if they are undefined
changed = true
}
}
if (changed) {
// setIfDifferent not required
set(initialData, k, newValue)
if (newValue !== initialValue) {
const propPath = [k]
patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue))
inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue))
}
}
}
// also emit a patch for modelType, since it will get included in the snapshot
const initialModelType = snapshotInitialData?.unprocessedModelType
const newModelType = modelInfo.name
if (initialModelType !== newModelType) {
const modelTypePath = [modelTypeKey]
patches.push(createPatchForObjectValueChange(modelTypePath, initialModelType, newModelType))
inversePatches.push(
createPatchForObjectValueChange(modelTypePath, newModelType, initialModelType)
)
}
finalizeNewModel(modelObj, initialData, modelClass)
emitPatches(modelObj, patches, inversePatches)
// type check it if needed
if (isModelAutoTypeCheckingEnabled() && getModelMetadata(modelClass).dataType) {
const err = modelObj.typeCheck()
if (err) {
err.throw()
}
}
}
)
function getModelDetails(modelClass: ModelClass<AnyModel>) {
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 = modelIdPropertyName ? modelProps[modelIdPropertyName] : undefined
return { modelInfo, modelIdPropertyName, modelProps, modelIdPropData }
}
function finalizeNewModel(
modelObj: O.Writable<AnyModel>,
initialData: any,
modelClass: ModelClass<AnyModel>
) {
tweakModel(modelObj, undefined)
// create observable data object with initial data
modelObj.$ = tweakPlainObject(
initialData,
{ parent: modelObj, path: "$" },
modelObj[modelTypeKey],
false,
true
)
if (inDevMode) {
makePropReadonly(modelObj, "$", true)
}
// run any extra initializers for the class as needed
applyModelInitializers(modelClass, modelObj)
}