diff --git a/packages/lib/src/model/BaseModel.ts b/packages/lib/src/model/BaseModel.ts index f2ceff5a..b3d4ea2e 100644 --- a/packages/lib/src/model/BaseModel.ts +++ b/packages/lib/src/model/BaseModel.ts @@ -20,7 +20,7 @@ import { assertIsObject, failure } from "../utils" import { getModelIdPropertyName } from "./getModelMetadata" import { modelIdKey, modelTypeKey } from "./metadata" import type { ModelConstructorOptions } from "./ModelConstructorOptions" -import { internalNewModel } from "./newModel" +import { internalFromSnapshotModel, internalNewModel } from "./newModel" import { assertIsModelClass } from "./utils" /** @@ -147,13 +147,14 @@ export abstract class BaseModel< // plain new assertIsObject(initialData, "initialData") - internalNewModel(this, observable.object(initialData as any, undefined, { deep: false }), { - modelClass, - generateNewIds: true, - }) + internalNewModel( + this, + observable.object(initialData as any, undefined, { deep: false }), + modelClass! + ) } else { // from snapshot - internalNewModel(this, undefined, { modelClass, snapshotInitialData, generateNewIds }) + internalFromSnapshotModel(this, snapshotInitialData!, modelClass!, !!generateNewIds) } } diff --git a/packages/lib/src/model/newModel.ts b/packages/lib/src/model/newModel.ts index 54d303f9..34df33eb 100644 --- a/packages/lib/src/model/newModel.ts +++ b/packages/lib/src/model/newModel.ts @@ -1,7 +1,7 @@ import { action, set } from "mobx" import type { O } from "ts-toolbelt" import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig" -import type { ModelCreationData } from "../modelShared/BaseModelShared" +import type { ModelClass, ModelCreationData } from "../modelShared/BaseModelShared" import { modelInfoByClass } from "../modelShared/modelInfo" import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo" import { applyModelInitializers } from "../modelShared/newModel" @@ -11,7 +11,7 @@ import { createPatchForObjectValueChange, emitPatches } from "../patch/emitPatch import { tweakModel } from "../tweaker/tweakModel" import { tweakPlainObject } from "../tweaker/tweakPlainObject" import { failure, inDevMode, makePropReadonly } from "../utils" -import { setIfDifferentWithReturn } from "../utils/setIfDifferent" +import { setIfDifferent, setIfDifferentWithReturn } from "../utils/setIfDifferent" import type { AnyModel } from "./BaseModel" import type { ModelConstructorOptions } from "./ModelConstructorOptions" import { getModelIdPropertyName, getModelMetadata } from "./getModelMetadata" @@ -25,63 +25,30 @@ export const internalNewModel = action( "newModel", ( origModelObj: M, - initialData: ModelCreationData | undefined, - options: Pick - ): M => { - const mode = initialData ? "new" : "fromSnapshot" - const { modelClass: _modelClass, snapshotInitialData, generateNewIds } = options - const modelClass = _modelClass! - + initialData: ModelCreationData, + modelClass: ModelClass + ): void => { if (inDevMode) { assertIsModelClass(modelClass, "modelClass") } - const modelObj = origModelObj as O.Writable + const { modelInfo, modelIdPropertyName, modelProps, modelIdPropData } = + getModelDetails(modelClass) - 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 - - let id: string | undefined - if (snapshotInitialData) { - 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) - } - - initialData = snapshotInitialData.snapshotToInitialData(sn) - } else { - // use symbol if provided - if (modelIdPropData && modelIdPropertyName) { - if (initialData![modelIdPropertyName]) { - id = initialData![modelIdPropertyName] - } else { - id = (modelIdPropData._defaultFn as () => string)() - } + // 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 modelObj[modelTypeKey] = modelInfo.name - const patches: Patch[] = [] - const inversePatches: Patch[] = [] - // fill in defaults in initial data const modelPropsKeys = Object.keys(modelProps) for (let i = 0; i < modelPropsKeys.length; i++) { @@ -94,12 +61,12 @@ export const internalNewModel = action( const propData = modelProps[k] - const initialValue = initialData![k] + const initialValue = initialData[k] let newValue = initialValue let changed = false // apply untransform (if any) if not in snapshot mode - if (mode === "new" && propData._transform) { + if (propData._transform) { changed = true newValue = propData._transform.untransform(newValue, modelObj, k) } @@ -118,22 +85,68 @@ export const internalNewModel = action( if (changed) { // setIfDifferent not required - set(initialData!, k, newValue) + set(initialData, k, newValue) + } + } - if (mode === "fromSnapshot" && newValue !== initialValue) { - const propPath = [k] + finalizeNewModel(modelObj, initialData, modelClass) - patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue)) - inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue)) - } + // type check it if needed + if (isModelAutoTypeCheckingEnabled() && getModelMetadata(modelClass).dataType) { + const err = modelObj.typeCheck() + if (err) { + err.throw() } } + } +) + +/** + * @internal + */ +export const internalFromSnapshotModel = action( + "fromSnapshotModel", + ( + origModelObj: M, + snapshotInitialData: NonNullable, + modelClass: ModelClass, + 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 + modelObj[modelTypeKey] = modelInfo.name + + const patches: Patch[] = [] + const inversePatches: Patch[] = [] if (modelIdPropertyName) { - const initialValue = initialData![modelIdPropertyName] + const initialValue = initialData[modelIdPropertyName] const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id) - if (valueChanged && mode === "fromSnapshot") { + if (valueChanged) { const modelIdPath = [modelIdPropertyName] patches.push(createPatchForObjectValueChange(modelIdPath, initialValue, id)) @@ -141,37 +154,60 @@ export const internalNewModel = action( } } - if (mode === "fromSnapshot") { - // 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) - ) + // 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 } - } - tweakModel(modelObj, undefined) + const propData = modelProps[k] - // create observable data object with initial data - modelObj.$ = tweakPlainObject( - initialData!, - { parent: modelObj, path: "$" }, - modelObj[modelTypeKey], - false, - true - ) + const initialValue = initialData[k] + let newValue = initialValue + let changed = false - if (inDevMode) { - makePropReadonly(modelObj, "$", true) + // 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) + ) } - // run any extra initializers for the class as needed - applyModelInitializers(modelClass, modelObj) + finalizeNewModel(modelObj, initialData, modelClass) emitPatches(modelObj, patches, inversePatches) @@ -182,7 +218,44 @@ export const internalNewModel = action( err.throw() } } - - return modelObj as M } ) + +function getModelDetails(modelClass: ModelClass) { + 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, + initialData: any, + modelClass: ModelClass +) { + 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) +}