mobx-keystone
Advanced tools
Comparing version 0.60.7 to 0.61.0
# Change Log | ||
## 0.61.0 | ||
- Added `withTransform()`, a new way to do property transforms, as well as the pre-made transforms `timestampToDateTransform`, `isoStringToDateTransform`, `objectToMapTransform`, `arrayToMapTransform` and `arrayToSetTransform`. See the docs `Maps, Sets, Dates` section for more info. | ||
- Data models can now use default property initializers. | ||
- Added `apply` and `applyComputed` to contexts so that they can be used to share information with models on creation time (kind of like `envs` in Mobx-state-tree). See the context docs for more details. | ||
- Fixed an issue with `asMap` where the map would not get updated when the backing array/object changed. | ||
- Fixed an issue with `asSet` where the set would not get updated when the backing array changed. | ||
## 0.60.7 | ||
@@ -4,0 +12,0 @@ |
@@ -81,3 +81,3 @@ import type { ActionMiddlewareDisposer } from "../action/middleware"; | ||
} & { | ||
$modelId: import("..").ModelProp<string, string, string, true, never>; | ||
$modelId: import("..").ModelProp<string, string, string, string, string, true, never>; | ||
}>; | ||
@@ -84,0 +84,0 @@ /** |
@@ -49,2 +49,22 @@ /** | ||
unset(node: object): void; | ||
/** | ||
* Applies a value override while the given function is running and, if a node is returned, | ||
* sets the node as a provider of the value. | ||
* | ||
* @typeparam R | ||
* @param fn Function to run. | ||
* @param value Value to apply. | ||
* @returns The value returned from the function. | ||
*/ | ||
apply<R>(fn: () => R, value: T): R; | ||
/** | ||
* Applies a computed value override while the given function is running and, if a node is returned, | ||
* sets the node as a provider of the comptued value. | ||
* | ||
* @typeparam R | ||
* @param fn Function to run. | ||
* @param value Value to apply. | ||
* @returns The value returned from the function. | ||
*/ | ||
applyComputed<R>(fn: () => R, valueFn: () => T): R; | ||
} | ||
@@ -51,0 +71,0 @@ /** |
@@ -1,2 +0,2 @@ | ||
import { dataTypeSymbol } from "../modelShared/BaseModelShared"; | ||
import { creationDataTypeSymbol, dataTypeSymbol, transformedCreationDataTypeSymbol, transformedDataTypeSymbol } from "../modelShared/BaseModelShared"; | ||
import type { TypeCheckError } from "../typeChecking/TypeCheckError"; | ||
@@ -12,4 +12,13 @@ /** | ||
[k: string]: any; | ||
}, CreationData extends { | ||
[k: string]: any; | ||
}, TransformedData extends { | ||
[k: string]: any; | ||
}, TransformedCreationData extends { | ||
[k: string]: any; | ||
}> { | ||
[dataTypeSymbol]: Data; | ||
[creationDataTypeSymbol]: Data; | ||
[transformedDataTypeSymbol]: TransformedData; | ||
[transformedCreationDataTypeSymbol]: Data; | ||
/** | ||
@@ -35,3 +44,3 @@ * Called after the instance is created when there's the first call to `fn(M, data)`. | ||
*/ | ||
constructor(data: Data); | ||
constructor(data: CreationData | TransformedCreationData); | ||
toString(options?: { | ||
@@ -48,3 +57,3 @@ withData?: boolean; | ||
*/ | ||
export interface AnyDataModel extends BaseDataModel<any> { | ||
export interface AnyDataModel extends BaseDataModel<any, any, any, any> { | ||
} | ||
@@ -51,0 +60,0 @@ /** |
import type { AbstractModelClass } from "../modelShared/BaseModelShared"; | ||
import type { ModelProps, ModelPropsToData, ModelPropsToSetter } from "../modelShared/prop"; | ||
import type { ModelProps, ModelPropsToCreationData, ModelPropsToData, ModelPropsToSetter, ModelPropsToTransformedCreationData, ModelPropsToTransformedData } from "../modelShared/prop"; | ||
import type { AnyDataModel, BaseDataModel, BaseDataModelKeys } from "./BaseDataModel"; | ||
export declare type _ComposedData<SuperModel, TProps extends ModelProps> = SuperModel extends BaseDataModel<infer D> ? ModelPropsToData<TProps> & D : ModelPropsToData<TProps>; | ||
export declare type _ComposedData<SuperModel, TProps extends ModelProps> = SuperModel extends BaseDataModel<any, infer CD, any, infer CTD> ? (ModelPropsToCreationData<TProps> & CD) | (ModelPropsToTransformedCreationData<TProps> & CTD) : ModelPropsToCreationData<TProps> | ModelPropsToTransformedCreationData<TProps>; | ||
export interface _DataModel<SuperModel, TProps extends ModelProps> { | ||
new (data: _ComposedData<SuperModel, TProps>): SuperModel & BaseDataModel<ModelPropsToData<TProps>> & Omit<ModelPropsToData<TProps>, BaseDataModelKeys> & ModelPropsToSetter<TProps>; | ||
new (data: _ComposedData<SuperModel, TProps>): SuperModel & BaseDataModel<ModelPropsToData<TProps>, ModelPropsToCreationData<TProps>, ModelPropsToTransformedData<TProps>, ModelPropsToTransformedCreationData<TProps>> & Omit<ModelPropsToTransformedData<TProps>, BaseDataModelKeys> & ModelPropsToSetter<TProps>; | ||
} | ||
@@ -8,0 +8,0 @@ /** |
@@ -16,2 +16,3 @@ export * from "./action"; | ||
export * from "./standardActions"; | ||
export * from "./transforms"; | ||
export * from "./treeUtils"; | ||
@@ -18,0 +19,0 @@ export * from "./tweaker"; |
import type { O } from "ts-toolbelt"; | ||
import { creationDataTypeSymbol, dataTypeSymbol, ModelClass } from "../modelShared/BaseModelShared"; | ||
import { creationDataTypeSymbol, dataTypeSymbol, ModelClass, transformedCreationDataTypeSymbol, transformedDataTypeSymbol } from "../modelShared/BaseModelShared"; | ||
import type { SnapshotInOfModel, SnapshotInOfObject, SnapshotOutOfModel } from "../snapshot/SnapshotOf"; | ||
@@ -23,5 +23,11 @@ import type { TypeCheckError } from "../typeChecking/TypeCheckError"; | ||
[k: string]: any; | ||
}, TransformedData extends { | ||
[k: string]: any; | ||
}, TransformedCreationData extends { | ||
[k: string]: any; | ||
}, ModelIdPropertyName extends string = typeof modelIdKey> { | ||
[dataTypeSymbol]: Data; | ||
[creationDataTypeSymbol]: CreationData; | ||
[transformedDataTypeSymbol]: TransformedData; | ||
[transformedCreationDataTypeSymbol]: TransformedCreationData; | ||
[modelIdPropertyNameSymbol]: ModelIdPropertyName; | ||
@@ -86,3 +92,3 @@ /** | ||
*/ | ||
constructor(data: CreationData); | ||
constructor(data: TransformedCreationData); | ||
toString(options?: { | ||
@@ -99,3 +105,3 @@ withData?: boolean; | ||
*/ | ||
export interface AnyModel extends BaseModel<any, any, any> { | ||
export interface AnyModel extends BaseModel<any, any, any, any, any> { | ||
} | ||
@@ -102,0 +108,0 @@ /** |
import type { AbstractModelClass } from "../modelShared/BaseModelShared"; | ||
import { idProp, ModelProps, ModelPropsToCreationData, ModelPropsToData, ModelPropsToSetter } from "../modelShared/prop"; | ||
import { idProp, ModelProps, ModelPropsToCreationData, ModelPropsToData, ModelPropsToSetter, ModelPropsToTransformedCreationData, ModelPropsToTransformedData } from "../modelShared/prop"; | ||
import type { AnyModel, BaseModel, BaseModelKeys, ModelIdPropertyName } from "./BaseModel"; | ||
export declare type _ComposedCreationData<SuperModel, TProps extends ModelProps> = SuperModel extends BaseModel<any, infer CD, any> ? ModelPropsToCreationData<TProps> & CD : ModelPropsToCreationData<TProps>; | ||
export declare type _ComposedCreationData<SuperModel, TProps extends ModelProps> = SuperModel extends BaseModel<any, any, any, infer TCD> ? ModelPropsToTransformedCreationData<TProps> & TCD : ModelPropsToTransformedCreationData<TProps>; | ||
export declare type _ModelId<SuperModel, TProps extends ModelProps> = SuperModel extends AnyModel ? ModelIdPropertyName<SuperModel> : ExtractModelIdProp<TProps> & string; | ||
export interface _Model<SuperModel, TProps extends ModelProps> { | ||
new (data: _ComposedCreationData<SuperModel, TProps>): SuperModel & BaseModel<ModelPropsToData<TProps>, ModelPropsToCreationData<TProps>, _ModelId<SuperModel, TProps>> & Omit<ModelPropsToData<TProps>, BaseModelKeys> & ModelPropsToSetter<TProps>; | ||
new (data: _ComposedCreationData<SuperModel, TProps>): SuperModel & BaseModel<ModelPropsToData<TProps>, ModelPropsToCreationData<TProps>, ModelPropsToTransformedData<TProps>, ModelPropsToTransformedCreationData<TProps>, _ModelId<SuperModel, TProps>> & Omit<ModelPropsToTransformedData<TProps>, BaseModelKeys> & ModelPropsToSetter<TProps>; | ||
} | ||
@@ -9,0 +9,0 @@ /** |
@@ -12,2 +12,10 @@ import type { AnyDataModel } from "../dataModel/BaseDataModel"; | ||
/** | ||
* @ignore | ||
*/ | ||
export declare const transformedDataTypeSymbol: unique symbol; | ||
/** | ||
* @ignore | ||
*/ | ||
export declare const transformedCreationDataTypeSymbol: unique symbol; | ||
/** | ||
* Extracts the instance type of a model class. | ||
@@ -29,4 +37,12 @@ */ | ||
*/ | ||
export declare type ModelCreationData<M extends AnyModel> = M[typeof creationDataTypeSymbol]; | ||
export declare type ModelCreationData<M extends AnyModel | AnyDataModel> = M[typeof creationDataTypeSymbol]; | ||
/** | ||
* The transformed data type of a model. | ||
*/ | ||
export declare type ModelTransformedData<M extends AnyModel | AnyDataModel> = M[typeof transformedDataTypeSymbol]; | ||
/** | ||
* The transformed creation data type of a model. | ||
*/ | ||
export declare type ModelTransformedCreationData<M extends AnyModel | AnyDataModel> = M[typeof transformedCreationDataTypeSymbol]; | ||
/** | ||
* Tricks Typescript into accepting a particular kind of generic class as a parameter for `ExtendedModel`. | ||
@@ -33,0 +49,0 @@ * Does nothing in runtime. |
import { AnyDataModel } from "../dataModel/BaseDataModel"; | ||
import { AnyModel } from "../model/BaseModel"; | ||
import { ModelClass, ModelData } from "./BaseModelShared"; | ||
import { ModelClass } from "./BaseModelShared"; | ||
import { AnyModelProp, ModelProps } from "./prop"; | ||
export declare function createModelPropDescriptor(modelPropName: string, modelProp: AnyModelProp | undefined, enumerable: boolean): PropertyDescriptor; | ||
export declare function getModelInstanceDataField<M extends AnyModel | AnyDataModel>(model: M, _modelProp: AnyModelProp | undefined, modelPropName: keyof ModelData<M>): ModelData<M>[typeof modelPropName]; | ||
export declare function setModelInstanceDataField<M extends AnyModel | AnyDataModel>(model: M, modelProp: AnyModelProp | undefined, modelPropName: keyof ModelData<M>, value: ModelData<M>[typeof modelPropName]): void; | ||
export declare function sharedInternalModel<TProps extends ModelProps, TBaseModel extends AnyModel | AnyDataModel>({ modelProps, baseModel, type, valueType, }: { | ||
@@ -9,0 +7,0 @@ modelProps: TProps; |
@@ -11,5 +11,7 @@ import type { O } from "ts-toolbelt"; | ||
*/ | ||
export interface ModelProp<TPropValue, TPropCreationValue, TIsOptional, TIsId extends boolean = false, THasSetter = never> { | ||
export interface ModelProp<TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsOptional, TIsId extends boolean = false, THasSetter = never> { | ||
$valueType: TPropValue; | ||
$creationValueType: TPropCreationValue; | ||
$transformedValueType: TTransformedValue; | ||
$transformedCreationValueType: TTransformedCreationValue; | ||
$isOptional: TIsOptional; | ||
@@ -23,12 +25,38 @@ $isId: TIsId; | ||
isId: boolean; | ||
withSetter(): ModelPropWithSetter<this>; | ||
transform: { | ||
transform(original: unknown, model: object, propName: PropertyKey, setOriginalValue: (newOriginalValue: unknown) => void): unknown; | ||
untransform(transformed: unknown, model: object, propName: PropertyKey): unknown; | ||
} | undefined; | ||
withSetter(): ModelProp<TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsOptional, TIsId, string>; | ||
/** | ||
* @deprecated Setter methods are preferred. | ||
*/ | ||
withSetter(mode: "assign"): ModelPropWithSetter<this>; | ||
withSetter(mode: "assign"): ModelProp<TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsOptional, TIsId, string>; | ||
/** | ||
* Sets a transform for the property. | ||
* | ||
* @typeparam TTV Transformed value type. | ||
* @param transform Transform to be used. | ||
* @returns | ||
*/ | ||
withTransform<TTV>(transform: ModelPropTransform<NonNullable<TPropValue>, TTV>): ModelProp<TPropValue, TPropCreationValue, TTV | Extract<TPropValue, null | undefined>, TTV | Extract<TPropCreationValue, null | undefined>, TIsOptional, TIsId, THasSetter>; | ||
} | ||
/** | ||
* A model prop transform. | ||
*/ | ||
export interface ModelPropTransform<TOriginal, TTransformed> { | ||
transform(params: { | ||
originalValue: TOriginal; | ||
cachedTransformedValue: TTransformed | undefined; | ||
setOriginalValue(value: TOriginal): void; | ||
}): TTransformed; | ||
untransform(params: { | ||
transformedValue: TTransformed; | ||
cacheTransformedValue(): void; | ||
}): TOriginal; | ||
} | ||
/** | ||
* Any model property. | ||
*/ | ||
export declare type AnyModelProp = ModelProp<any, any, any, any, any>; | ||
export declare type AnyModelProp = ModelProp<any, any, any, any, any, any, any>; | ||
/** | ||
@@ -51,4 +79,12 @@ * Model properties. | ||
}, OptionalModelProps<MP>>; | ||
export declare type ModelPropsToTransformedData<MP extends ModelProps> = { | ||
[k in keyof MP]: MP[k]["$transformedValueType"]; | ||
}; | ||
export declare type ModelPropsToTransformedCreationData<MP extends ModelProps> = { | ||
[k in keyof MP]?: MP[k]["$transformedCreationValueType"]; | ||
} & O.Omit<{ | ||
[k in keyof MP]: MP[k]["$transformedCreationValueType"]; | ||
}, OptionalModelProps<MP>>; | ||
export declare type ModelPropsToSetter<MP extends ModelProps> = { | ||
[k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize<k & string>}`]: (value: MP[k]["$valueType"]) => void; | ||
[k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize<k & string>}`]: (value: MP[k]["$transformedValueType"]) => void; | ||
}; | ||
@@ -59,3 +95,3 @@ /** | ||
*/ | ||
export declare const idProp: ModelProp<string, string, string, true, never>; | ||
export declare const idProp: ModelProp<string, string, string, string, string, true, never>; | ||
/** | ||
@@ -68,14 +104,8 @@ * @ignore | ||
*/ | ||
export declare type MaybeOptionalModelProp<TPropValue> = ModelProp<TPropValue, TPropValue, IsOptionalValue<TPropValue, string, never>>; | ||
export declare type MaybeOptionalModelProp<TPropValue> = ModelProp<TPropValue, TPropValue, TPropValue, TPropValue, IsOptionalValue<TPropValue, string, never>>; | ||
/** | ||
* A model prop that is definitely optional. | ||
*/ | ||
export declare type OptionalModelProp<TPropValue> = ModelProp<TPropValue, TPropValue | null | undefined, string>; | ||
export declare type OptionalModelProp<TPropValue> = ModelProp<TPropValue, TPropValue | null | undefined, TPropValue, TPropValue | null | undefined, string>; | ||
/** | ||
* A model prop with a generated setter. | ||
*/ | ||
export declare type ModelPropWithSetter<MP extends AnyModelProp> = Omit<MP, "$hasSetter"> & { | ||
$hasSetter: string; | ||
}; | ||
/** | ||
* Defines a model property, with an optional function to generate a default value | ||
@@ -82,0 +112,0 @@ * if the input snapshot / model creation data is `null` or `undefined`. |
@@ -8,3 +8,3 @@ import type { ModelClass } from "../modelShared/BaseModelShared"; | ||
} & { | ||
$modelId: import("..").ModelProp<string, string, string, true, never>; | ||
$modelId: import("..").ModelProp<string, string, string, string, string, true, never>; | ||
}>; | ||
@@ -11,0 +11,0 @@ /** |
@@ -1,2 +0,2 @@ | ||
export declare function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V; | ||
export declare function getOrCreate<K extends object, V>(map: WeakMap<K, V>, key: K, create: () => V): V; | ||
export declare function getOrCreate<K, V, C = V>(map: Map<K, V>, key: K, create: () => C): V; | ||
export declare function getOrCreate<K extends object, V, C = V>(map: WeakMap<K, V>, key: K, create: () => C): V; |
declare const ArraySet_base: import("../model/Model")._Model<unknown, { | ||
items: import("..").OptionalModelProp<any[]>; | ||
} & { | ||
$modelId: import("..").ModelProp<string, string, string, true, never>; | ||
$modelId: import("..").ModelProp<string, string, string, string, string, true, never>; | ||
}>; | ||
@@ -6,0 +6,0 @@ /** |
@@ -7,4 +7,4 @@ import { ObservableMap } from "mobx"; | ||
*/ | ||
export declare function asMap<T>(array: Array<[string, T]>): ObservableMap<string, T> & { | ||
dataObject: Array<[string, T]>; | ||
export declare function asMap<K, V>(array: Array<[K, V]>): ObservableMap<K, V> & { | ||
dataObject: Array<[K, V]>; | ||
}; | ||
@@ -30,2 +30,2 @@ /** | ||
*/ | ||
export declare function mapToArray<T>(map: Map<string, T>): Array<[string, T]>; | ||
export declare function mapToArray<K, V>(map: Map<K, V>): Array<[K, V]>; |
@@ -6,3 +6,3 @@ declare const ObjectMap_base: import("../model/Model")._Model<unknown, { | ||
} & { | ||
$modelId: import("..").ModelProp<string, string, string, true, never>; | ||
$modelId: import("..").ModelProp<string, string, string, string, string, true, never>; | ||
}>; | ||
@@ -9,0 +9,0 @@ /** |
{ | ||
"name": "mobx-keystone", | ||
"version": "0.60.7", | ||
"version": "0.61.0", | ||
"description": "A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more", | ||
@@ -70,12 +70,12 @@ "keywords": [ | ||
"@types/jest": "^27.0.1", | ||
"@types/node": "^16.6.1", | ||
"@types/node": "^16.7.1", | ||
"jest": "^27.0.6", | ||
"mobx": "^6.3.2", | ||
"netlify-cli": "^6.5.1", | ||
"netlify-cli": "^6.7.3", | ||
"shx": "^0.3.3", | ||
"spec.ts": "^1.1.3", | ||
"ts-jest": "^27.0.5", | ||
"ts-node": "^10.2.0", | ||
"typedoc": "^0.21.5", | ||
"typescript": "^4.2.4", | ||
"ts-node": "^10.2.1", | ||
"typedoc": "^0.21.6", | ||
"typescript": "4.2.4", | ||
"vite": "^2.5.0" | ||
@@ -82,0 +82,0 @@ }, |
@@ -15,3 +15,3 @@ import { remove } from "mobx" | ||
export function applyDelete<O extends object, K extends keyof O>(node: O, fieldName: K): void { | ||
assertTweakedObject(node, "node") | ||
assertTweakedObject(node, "node", true) | ||
@@ -18,0 +18,0 @@ wrappedInternalApplyDelete().call(node, fieldName as string | number) |
@@ -21,3 +21,3 @@ import { isObservable, set } from "mobx" | ||
): void { | ||
assertTweakedObject(node, "node") | ||
assertTweakedObject(node, "node", true) | ||
@@ -24,0 +24,0 @@ wrappedInternalApplySet().call(node, fieldName as string | number, value) |
import { action, computed, createAtom, IAtom, IComputedValue, observable } from "mobx" | ||
import { fastGetParent } from "../parent/path" | ||
import { assertTweakedObject } from "../tweaker/core" | ||
import { assertTweakedObject, isTweakedObject } from "../tweaker/core" | ||
import { getMobxVersion, mobx6 } from "../utils" | ||
@@ -62,2 +62,24 @@ import { getOrCreate } from "../utils/mapUtils" | ||
unset(node: object): void | ||
/** | ||
* Applies a value override while the given function is running and, if a node is returned, | ||
* sets the node as a provider of the value. | ||
* | ||
* @typeparam R | ||
* @param fn Function to run. | ||
* @param value Value to apply. | ||
* @returns The value returned from the function. | ||
*/ | ||
apply<R>(fn: () => R, value: T): R | ||
/** | ||
* Applies a computed value override while the given function is running and, if a node is returned, | ||
* sets the node as a provider of the comptued value. | ||
* | ||
* @typeparam R | ||
* @param fn Function to run. | ||
* @param value Value to apply. | ||
* @returns The value returned from the function. | ||
*/ | ||
applyComputed<R>(fn: () => R, valueFn: () => T): R | ||
} | ||
@@ -75,3 +97,3 @@ | ||
function getContextValue<T>(contextValue: ContextValue<T>): T { | ||
function resolveContextValue<T>(contextValue: ContextValue<T>): T { | ||
if (contextValue.type === "value") { | ||
@@ -88,2 +110,5 @@ return contextValue.value | ||
@observable.ref | ||
private overrideContextValue: ContextValue<T> | undefined | ||
private readonly nodeContextValue = new WeakMap<object, ContextValue<T>>() | ||
@@ -101,3 +126,3 @@ private readonly nodeAtom = new WeakMap<object, IAtom>() | ||
if (obsForNode) { | ||
return getContextValue(obsForNode) | ||
return resolveContextValue(obsForNode) | ||
} | ||
@@ -107,2 +132,5 @@ | ||
if (!parent) { | ||
if (this.overrideContextValue) { | ||
return resolveContextValue(this.overrideContextValue) | ||
} | ||
return this.getDefault() | ||
@@ -143,3 +171,3 @@ } | ||
getDefault(): T { | ||
return getContextValue(this.defaultContextValue) | ||
return resolveContextValue(this.defaultContextValue) | ||
} | ||
@@ -174,7 +202,6 @@ | ||
@action | ||
setComputed(node: object, valueFn: () => T) { | ||
private _setComputed(node: object, computedValueFn: IComputedValue<T>) { | ||
assertTweakedObject(node, "node") | ||
this.nodeContextValue.set(node, { type: "computed", value: computed(valueFn) }) | ||
this.nodeContextValue.set(node, { type: "computed", value: computedValueFn }) | ||
this.getNodeAtom(node).reportChanged() | ||
@@ -184,2 +211,7 @@ } | ||
@action | ||
setComputed(node: object, valueFn: () => T) { | ||
this._setComputed(node, computed(valueFn)) | ||
} | ||
@action | ||
unset(node: object) { | ||
@@ -192,2 +224,42 @@ assertTweakedObject(node, "node") | ||
@action | ||
apply<R>(fn: () => R, value: T): R { | ||
const old = this.overrideContextValue | ||
this.overrideContextValue = { | ||
type: "value", | ||
value, | ||
} | ||
try { | ||
const ret = fn() | ||
if (isTweakedObject(ret, true)) { | ||
this.set(ret, value) | ||
} | ||
return ret | ||
} finally { | ||
this.overrideContextValue = old | ||
} | ||
} | ||
@action | ||
applyComputed<R>(fn: () => R, valueFn: () => T): R { | ||
const computedValueFn = computed(valueFn) | ||
const old = this.overrideContextValue | ||
this.overrideContextValue = { | ||
type: "computed", | ||
value: computedValueFn, | ||
} | ||
try { | ||
const ret = fn() | ||
if (isTweakedObject(ret, true)) { | ||
this._setComputed(ret, computedValueFn) | ||
} | ||
return ret | ||
} finally { | ||
this.overrideContextValue = old | ||
} | ||
} | ||
constructor(defaultValue?: T) { | ||
@@ -194,0 +266,0 @@ if (getMobxVersion() >= 6) { |
@@ -1,4 +0,13 @@ | ||
import { dataTypeSymbol, ModelClass } from "../modelShared/BaseModelShared" | ||
import { | ||
creationDataTypeSymbol, | ||
dataTypeSymbol, | ||
ModelClass, | ||
transformedCreationDataTypeSymbol, | ||
transformedDataTypeSymbol, | ||
} from "../modelShared/BaseModelShared" | ||
import { modelInfoByClass } from "../modelShared/modelInfo" | ||
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo" | ||
import { noDefaultValue } from "../modelShared/prop" | ||
import { getSnapshot } from "../snapshot/getSnapshot" | ||
import { isTreeNode } from "../tweaker/core" | ||
import { toTreeNode } from "../tweaker/tweak" | ||
@@ -23,5 +32,13 @@ import { typesDataModelData } from "../typeChecking/dataModelData" | ||
*/ | ||
export abstract class BaseDataModel<Data extends { [k: string]: any }> { | ||
export abstract class BaseDataModel< | ||
Data extends { [k: string]: any }, | ||
CreationData extends { [k: string]: any }, | ||
TransformedData extends { [k: string]: any }, | ||
TransformedCreationData extends { [k: string]: any } | ||
> { | ||
// just to make typing work properly | ||
[dataTypeSymbol]: Data | ||
[dataTypeSymbol]: Data; | ||
[creationDataTypeSymbol]: Data; | ||
[transformedDataTypeSymbol]: TransformedData; | ||
[transformedCreationDataTypeSymbol]: Data | ||
@@ -54,7 +71,6 @@ /** | ||
*/ | ||
constructor(data: Data) { | ||
constructor(data: CreationData | TransformedCreationData) { | ||
if (!isObject(data)) { | ||
throw failure("data models can only work over data objects") | ||
} | ||
const tweakedData = toTreeNode(data) | ||
@@ -64,2 +80,51 @@ const { modelClass: _modelClass }: DataModelConstructorOptions = arguments[1] as any | ||
let tweakedData: Data | ||
if (isTreeNode(data)) { | ||
// in theory already initialized | ||
tweakedData = data as any as Data | ||
} else { | ||
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 modelProps = getInternalModelClassPropsInfo(modelClass) | ||
const initialData: Record<string, any> = Object.assign({}, data) | ||
const modelPropsKeys = Object.keys(modelProps) | ||
for (let i = 0; i < modelPropsKeys.length; i++) { | ||
const k = modelPropsKeys[i] | ||
const propData = modelProps[k] | ||
let newValue = initialData![k] | ||
let changed = false | ||
// apply untransform (if any) | ||
if (propData.transform) { | ||
changed = true | ||
newValue = propData.transform.untransform(newValue, this, k) | ||
} | ||
// apply default value (if needed) | ||
if (newValue == null) { | ||
if (propData.defaultFn !== noDefaultValue) { | ||
changed = true | ||
newValue = propData.defaultFn() | ||
} else if (propData.defaultValue !== noDefaultValue) { | ||
changed = true | ||
newValue = propData.defaultValue | ||
} | ||
} | ||
if (changed) { | ||
initialData[k] = newValue | ||
} | ||
} | ||
tweakedData = toTreeNode(initialData as Data) | ||
} | ||
const instancesForModelClass = getOrCreate( | ||
@@ -84,2 +149,5 @@ dataModelInstanceCache, | ||
delete self[dataTypeSymbol] | ||
delete self[creationDataTypeSymbol] | ||
delete self[transformedDataTypeSymbol] | ||
delete self[transformedCreationDataTypeSymbol] | ||
@@ -123,3 +191,3 @@ internalNewDataModel(this, tweakedData as any, { | ||
*/ | ||
export interface AnyDataModel extends BaseDataModel<any> {} | ||
export interface AnyDataModel extends BaseDataModel<any, any, any, any> {} | ||
@@ -126,0 +194,0 @@ /** |
import type { AbstractModelClass, ModelClass } from "../modelShared/BaseModelShared" | ||
import { sharedInternalModel } from "../modelShared/Model" | ||
import type { ModelProps, ModelPropsToData, ModelPropsToSetter } from "../modelShared/prop" | ||
import type { | ||
ModelProps, | ||
ModelPropsToCreationData, | ||
ModelPropsToData, | ||
ModelPropsToSetter, | ||
ModelPropsToTransformedCreationData, | ||
ModelPropsToTransformedData, | ||
} from "../modelShared/prop" | ||
import type { AnyDataModel, BaseDataModel, BaseDataModelKeys } from "./BaseDataModel" | ||
@@ -8,11 +15,19 @@ import { assertIsDataModelClass, isDataModelClass } from "./utils" | ||
export type _ComposedData<SuperModel, TProps extends ModelProps> = SuperModel extends BaseDataModel< | ||
infer D | ||
any, | ||
infer CD, | ||
any, | ||
infer CTD | ||
> | ||
? ModelPropsToData<TProps> & D | ||
: ModelPropsToData<TProps> | ||
? (ModelPropsToCreationData<TProps> & CD) | (ModelPropsToTransformedCreationData<TProps> & CTD) | ||
: ModelPropsToCreationData<TProps> | ModelPropsToTransformedCreationData<TProps> | ||
export interface _DataModel<SuperModel, TProps extends ModelProps> { | ||
new (data: _ComposedData<SuperModel, TProps>): SuperModel & | ||
BaseDataModel<ModelPropsToData<TProps>> & | ||
Omit<ModelPropsToData<TProps>, BaseDataModelKeys> & | ||
BaseDataModel< | ||
ModelPropsToData<TProps>, | ||
ModelPropsToCreationData<TProps>, | ||
ModelPropsToTransformedData<TProps>, | ||
ModelPropsToTransformedCreationData<TProps> | ||
> & | ||
Omit<ModelPropsToTransformedData<TProps>, BaseDataModelKeys> & | ||
ModelPropsToSetter<TProps> | ||
@@ -34,5 +49,3 @@ } | ||
>( | ||
genFn: ( | ||
...args: A | ||
) => { | ||
genFn: (...args: A) => { | ||
baseModel: AbstractModelClass<TModel> | ||
@@ -39,0 +52,0 @@ props: TProps |
@@ -16,2 +16,3 @@ export * from "./action" | ||
export * from "./standardActions" | ||
export * from "./transforms" | ||
export * from "./treeUtils" | ||
@@ -18,0 +19,0 @@ export * from "./tweaker" |
import { observable } from "mobx" | ||
import type { O } from "ts-toolbelt" | ||
import { getGlobalConfig } from "../globalConfig" | ||
import { creationDataTypeSymbol, dataTypeSymbol, ModelClass } from "../modelShared/BaseModelShared" | ||
import { | ||
creationDataTypeSymbol, | ||
dataTypeSymbol, | ||
ModelClass, | ||
transformedCreationDataTypeSymbol, | ||
transformedDataTypeSymbol, | ||
} from "../modelShared/BaseModelShared" | ||
import { modelInfoByClass } from "../modelShared/modelInfo" | ||
@@ -40,2 +46,4 @@ import { getSnapshot } from "../snapshot/getSnapshot" | ||
CreationData extends { [k: string]: any }, | ||
TransformedData extends { [k: string]: any }, | ||
TransformedCreationData extends { [k: string]: any }, | ||
ModelIdPropertyName extends string = typeof modelIdKey | ||
@@ -46,2 +54,4 @@ > { | ||
[creationDataTypeSymbol]: CreationData; | ||
[transformedDataTypeSymbol]: TransformedData; | ||
[transformedCreationDataTypeSymbol]: TransformedCreationData; | ||
[modelIdPropertyNameSymbol]: ModelIdPropertyName; | ||
@@ -105,5 +115,3 @@ | ||
*/ | ||
fromSnapshot?(snapshot: { | ||
[k: string]: any | ||
}): SnapshotInOfObject<CreationData> & { | ||
fromSnapshot?(snapshot: { [k: string]: any }): SnapshotInOfObject<CreationData> & { | ||
[modelTypeKey]?: string | ||
@@ -126,9 +134,6 @@ } | ||
*/ | ||
constructor(data: CreationData) { | ||
constructor(data: TransformedCreationData) { | ||
let initialData = data as any | ||
const { | ||
snapshotInitialData, | ||
modelClass, | ||
generateNewIds, | ||
}: ModelConstructorOptions = arguments[1] as any | ||
const { snapshotInitialData, modelClass, generateNewIds }: ModelConstructorOptions = | ||
arguments[1] as any | ||
@@ -142,2 +147,4 @@ Object.setPrototypeOf(this, modelClass!.prototype) | ||
delete self[creationDataTypeSymbol] | ||
delete self[transformedDataTypeSymbol] | ||
delete self[transformedCreationDataTypeSymbol] | ||
delete self[modelIdPropertyNameSymbol] | ||
@@ -198,3 +205,3 @@ | ||
*/ | ||
export interface AnyModel extends BaseModel<any, any, any> {} | ||
export interface AnyModel extends BaseModel<any, any, any, any, any> {} | ||
@@ -201,0 +208,0 @@ /** |
@@ -9,2 +9,4 @@ import type { AbstractModelClass, ModelClass } from "../modelShared/BaseModelShared" | ||
ModelPropsToSetter, | ||
ModelPropsToTransformedCreationData, | ||
ModelPropsToTransformedData, | ||
} from "../modelShared/prop" | ||
@@ -17,5 +19,5 @@ import type { AnyModel, BaseModel, BaseModelKeys, ModelIdPropertyName } from "./BaseModel" | ||
TProps extends ModelProps | ||
> = SuperModel extends BaseModel<any, infer CD, any> | ||
? ModelPropsToCreationData<TProps> & CD | ||
: ModelPropsToCreationData<TProps> | ||
> = SuperModel extends BaseModel<any, any, any, infer TCD> | ||
? ModelPropsToTransformedCreationData<TProps> & TCD | ||
: ModelPropsToTransformedCreationData<TProps> | ||
@@ -31,5 +33,7 @@ export type _ModelId<SuperModel, TProps extends ModelProps> = SuperModel extends AnyModel | ||
ModelPropsToCreationData<TProps>, | ||
ModelPropsToTransformedData<TProps>, | ||
ModelPropsToTransformedCreationData<TProps>, | ||
_ModelId<SuperModel, TProps> | ||
> & | ||
Omit<ModelPropsToData<TProps>, BaseModelKeys> & | ||
Omit<ModelPropsToTransformedData<TProps>, BaseModelKeys> & | ||
ModelPropsToSetter<TProps> | ||
@@ -42,7 +46,6 @@ } | ||
*/ | ||
export type AddModelIdPropIfNeeded< | ||
TProps extends ModelProps | ||
> = ExtractModelIdProp<TProps> extends never | ||
? TProps & { $modelId: typeof idProp } // we use the actual name here to avoid having to re-export the original | ||
: TProps | ||
export type AddModelIdPropIfNeeded<TProps extends ModelProps> = | ||
ExtractModelIdProp<TProps> extends never | ||
? TProps & { $modelId: typeof idProp } // we use the actual name here to avoid having to re-export the original | ||
: TProps | ||
@@ -66,5 +69,3 @@ /** | ||
export function ExtendedModel<TProps extends ModelProps, TModel extends AnyModel, A extends []>( | ||
genFn: ( | ||
...args: A | ||
) => { | ||
genFn: (...args: A) => { | ||
baseModel: AbstractModelClass<TModel> | ||
@@ -71,0 +72,0 @@ props: TProps |
@@ -8,3 +8,2 @@ import type { ModelClass } from "../modelShared/BaseModelShared" | ||
*/ | ||
export interface ModelConstructorOptions { | ||
@@ -11,0 +10,0 @@ snapshotInitialData?: { |
import { action, set } from "mobx" | ||
import type { O } from "ts-toolbelt" | ||
import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig" | ||
import type { ModelCreationData } from "../modelShared/BaseModelShared" | ||
import type { ModelTransformedCreationData } from "../modelShared/BaseModelShared" | ||
import { modelInfoByClass } from "../modelShared/modelInfo" | ||
@@ -26,5 +26,6 @@ import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo" | ||
origModelObj: M, | ||
initialData: ModelCreationData<M> | undefined, | ||
initialData: ModelTransformedCreationData<M> | undefined, | ||
options: Pick<ModelConstructorOptions, "modelClass" | "snapshotInitialData" | "generateNewIds"> | ||
): M => { | ||
const mode = initialData ? "new" : "fromSnapshot" | ||
const { modelClass: _modelClass, snapshotInitialData, generateNewIds } = options | ||
@@ -80,16 +81,33 @@ const modelClass = _modelClass! | ||
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) | ||
if (k === modelIdPropertyName) { | ||
continue | ||
} | ||
const propData = modelProps[k] | ||
let newValue = initialData![k] | ||
let changed = false | ||
// apply untransform (if any) | ||
if (mode === "new" && propData.transform) { | ||
changed = true | ||
newValue = propData.transform.untransform(newValue, modelObj, k) | ||
} | ||
// apply default value (if needed) | ||
if (newValue == null) { | ||
if (propData.defaultFn !== noDefaultValue) { | ||
changed = true | ||
newValue = propData.defaultFn() | ||
} else if (propData.defaultValue !== noDefaultValue) { | ||
changed = true | ||
newValue = propData.defaultValue | ||
} | ||
} | ||
if (changed) { | ||
set(initialData as any, k, newValue) | ||
} | ||
} | ||
@@ -96,0 +114,0 @@ |
@@ -16,2 +16,12 @@ import type { AnyDataModel } from "../dataModel/BaseDataModel" | ||
* @ignore | ||
*/ | ||
export const transformedDataTypeSymbol = Symbol() | ||
/** | ||
* @ignore | ||
*/ | ||
export const transformedCreationDataTypeSymbol = Symbol() | ||
/** | ||
* @ignore | ||
* @internal | ||
@@ -24,3 +34,3 @@ */ | ||
*/ | ||
export type ModelClass<M extends AnyModel | AnyDataModel> = { | ||
export type ModelClass<M extends AnyModel | AnyDataModel> = { | ||
new (initialData: any): M | ||
@@ -32,3 +42,5 @@ } | ||
*/ | ||
export type AbstractModelClass<M extends AnyModel|AnyDataModel> = abstract new (initialData: any) => M; | ||
export type AbstractModelClass<M extends AnyModel | AnyDataModel> = abstract new ( | ||
initialData: any | ||
) => M | ||
@@ -43,7 +55,17 @@ /** | ||
*/ | ||
export type ModelCreationData< | ||
M extends AnyModel | ||
> = M[typeof creationDataTypeSymbol] | ||
export type ModelCreationData<M extends AnyModel | AnyDataModel> = M[typeof creationDataTypeSymbol] | ||
/** | ||
* The transformed data type of a model. | ||
*/ | ||
export type ModelTransformedData<M extends AnyModel | AnyDataModel> = | ||
M[typeof transformedDataTypeSymbol] | ||
/** | ||
* The transformed creation data type of a model. | ||
*/ | ||
export type ModelTransformedCreationData<M extends AnyModel | AnyDataModel> = | ||
M[typeof transformedCreationDataTypeSymbol] | ||
/** | ||
* Tricks Typescript into accepting a particular kind of generic class as a parameter for `ExtendedModel`. | ||
@@ -56,4 +78,6 @@ * Does nothing in runtime. | ||
*/ | ||
export function modelClass<T extends AnyModel | AnyDataModel>(type: { prototype: T }): ModelClass<T> { | ||
export function modelClass<T extends AnyModel | AnyDataModel>(type: { | ||
prototype: T | ||
}): ModelClass<T> { | ||
return type as any | ||
} |
@@ -7,3 +7,2 @@ import { applySet } from "../action/applySet" | ||
import type { DataModelMetadata } from "../dataModel/getDataModelMetadata" | ||
import { isDataModel } from "../dataModel/utils" | ||
import { getGlobalConfig } from "../globalConfig/globalConfig" | ||
@@ -21,7 +20,7 @@ import { AnyModel, BaseModel, baseModelPropNames } from "../model/BaseModel" | ||
import { assertIsObject, failure, propNameToSetterName } from "../utils" | ||
import { ModelClass, ModelData, modelInitializedSymbol } from "./BaseModelShared" | ||
import { ModelClass, modelInitializedSymbol, ModelTransformedData } from "./BaseModelShared" | ||
import { modelInitializersSymbol } from "./modelClassInitializer" | ||
import { getInternalModelClassPropsInfo, setInternalModelClassPropsInfo } from "./modelPropsInfo" | ||
import { modelMetadataSymbol, modelUnwrappedClassSymbol } from "./modelSymbols" | ||
import { AnyModelProp, ModelProps, noDefaultValue, prop } from "./prop" | ||
import { AnyModelProp, ModelProps, prop } from "./prop" | ||
import { assertIsClassOrDataModelClass } from "./utils" | ||
@@ -61,29 +60,38 @@ | ||
export function getModelInstanceDataField<M extends AnyModel | AnyDataModel>( | ||
function getModelInstanceDataField<M extends AnyModel | AnyDataModel>( | ||
model: M, | ||
_modelProp: AnyModelProp | undefined, | ||
modelPropName: keyof ModelData<M> | ||
): ModelData<M>[typeof modelPropName] { | ||
modelProp: AnyModelProp | undefined, | ||
modelPropName: keyof ModelTransformedData<M> | ||
): ModelTransformedData<M>[typeof modelPropName] { | ||
// no need to use get since these vars always get on the initial $ | ||
return model.$[modelPropName] | ||
const value = model.$[modelPropName] | ||
if (modelProp?.transform) { | ||
return modelProp.transform.transform(value, model, modelPropName, (newValue) => { | ||
// use apply set instead to wrap it in an action | ||
// set the $ object to set the original value directly | ||
applySet(model.$, modelPropName, newValue) | ||
}) | ||
} | ||
return value | ||
} | ||
export function setModelInstanceDataField<M extends AnyModel | AnyDataModel>( | ||
function setModelInstanceDataField<M extends AnyModel | AnyDataModel>( | ||
model: M, | ||
modelProp: AnyModelProp | undefined, | ||
modelPropName: keyof ModelData<M>, | ||
value: ModelData<M>[typeof modelPropName] | ||
modelPropName: keyof ModelTransformedData<M>, | ||
value: ModelTransformedData<M>[typeof modelPropName] | ||
): void { | ||
if (modelProp?.setter === "assign" && !getCurrentActionContext()) { | ||
// use apply set instead to wrap it in an action | ||
if (isDataModel(model)) { | ||
applySet(model.$, modelPropName as any, value) | ||
} else { | ||
applySet(model, modelPropName as any, value) | ||
} | ||
applySet(model, modelPropName, value) | ||
return | ||
} | ||
const transformedValue = modelProp?.transform | ||
? modelProp.transform.untransform(value, model, modelPropName) | ||
: value | ||
// no need to use set since these vars always get on the initial $ | ||
model.$[modelPropName] = value | ||
model.$[modelPropName] = transformedValue | ||
} | ||
@@ -156,13 +164,2 @@ | ||
if (type === "data") { | ||
// make sure props have no defaults | ||
for (const [k, mp] of Object.entries(composedModelProps)) { | ||
if (mp.defaultValue !== noDefaultValue || mp.defaultFn !== noDefaultValue) { | ||
throw failure( | ||
`data models do not support properties with default values, but property '${k}' has one` | ||
) | ||
} | ||
} | ||
} | ||
const needsTypeChecker = Object.values(composedModelProps).some((mp) => !!mp.typeChecker) | ||
@@ -169,0 +166,0 @@ |
@@ -32,110 +32,111 @@ import { HookAction } from "../action/hookActions" | ||
*/ | ||
export const model = (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>( | ||
clazz: MC | ||
): MC => { | ||
return internalModel(name)(clazz) | ||
} | ||
const internalModel = (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>( | ||
clazz: MC | ||
): MC => { | ||
const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined | ||
if (!type) { | ||
throw failure(`clazz must be a class that extends from Model/DataModel`) | ||
export const model = | ||
(name: string) => | ||
<MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC): MC => { | ||
return internalModel(name)(clazz) | ||
} | ||
if (modelInfoByName[name]) { | ||
if (getGlobalConfig().showDuplicateModelNameWarnings) { | ||
logWarning( | ||
"warn", | ||
`a model with name "${name}" already exists (if you are using hot-reloading you may safely ignore this warning)`, | ||
`duplicateModelName - ${name}` | ||
) | ||
const internalModel = | ||
(name: string) => | ||
<MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC): MC => { | ||
const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined | ||
if (!type) { | ||
throw failure(`clazz must be a class that extends from Model/DataModel`) | ||
} | ||
} | ||
if ((clazz as any)[modelUnwrappedClassSymbol]) { | ||
throw failure("a class already decorated with `@model` cannot be re-decorated") | ||
} | ||
if (modelInfoByName[name]) { | ||
if (getGlobalConfig().showDuplicateModelNameWarnings) { | ||
logWarning( | ||
"warn", | ||
`a model with name "${name}" already exists (if you are using hot-reloading you may safely ignore this warning)`, | ||
`duplicateModelName - ${name}` | ||
) | ||
} | ||
} | ||
// trick so plain new works | ||
const newClazz: any = function (this: any, initialData: any, modelConstructorOptions: any) { | ||
const instance = new (clazz as any)(initialData, modelConstructorOptions) | ||
if ((clazz as any)[modelUnwrappedClassSymbol]) { | ||
throw failure("a class already decorated with `@model` cannot be re-decorated") | ||
} | ||
// set or else it points to the undecorated class | ||
Object.defineProperty(instance, "constructor", { | ||
configurable: true, | ||
writable: true, | ||
enumerable: false, | ||
value: newClazz, | ||
}) | ||
// trick so plain new works | ||
const newClazz: any = function (this: any, initialData: any, modelConstructorOptions: any) { | ||
const instance = new (clazz as any)(initialData, modelConstructorOptions) | ||
runLateInitializationFunctions(instance, runAfterNewSymbol) | ||
// set or else it points to the undecorated class | ||
Object.defineProperty(instance, "constructor", { | ||
configurable: true, | ||
writable: true, | ||
enumerable: false, | ||
value: newClazz, | ||
}) | ||
// compatibility with mobx 6 | ||
if (getMobxVersion() >= 6) { | ||
try { | ||
mobx6.makeObservable(instance) | ||
} catch (err) { | ||
// sadly we need to use this hack since the PR to do this the proper way | ||
// was rejected on the mobx side | ||
if ( | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" | ||
) { | ||
throw err | ||
runLateInitializationFunctions(instance, runAfterNewSymbol) | ||
// compatibility with mobx 6 | ||
if (getMobxVersion() >= 6) { | ||
try { | ||
mobx6.makeObservable(instance) | ||
} catch (e) { | ||
const err = e as Error | ||
// sadly we need to use this hack since the PR to do this the proper way | ||
// was rejected on the mobx side | ||
if ( | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && | ||
err.message !== | ||
"[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" | ||
) { | ||
throw err | ||
} | ||
} | ||
} | ||
} | ||
// the object is ready | ||
addHiddenProp(instance, modelInitializedSymbol, true, false) | ||
// the object is ready | ||
addHiddenProp(instance, modelInitializedSymbol, true, false) | ||
if (type === "class" && instance.onInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) | ||
if (type === "class" && instance.onInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) | ||
instance.onInit() | ||
} | ||
instance.onInit() | ||
} | ||
if (type === "data" && instance.onLazyInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) | ||
if (type === "data" && instance.onLazyInit) { | ||
wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) | ||
instance.onLazyInit() | ||
instance.onLazyInit() | ||
} | ||
return instance | ||
} | ||
return instance | ||
} | ||
clazz.toString = () => `class ${clazz.name}#${name}` | ||
if (type === "class") { | ||
;(clazz as any)[modelTypeKey] = name | ||
} | ||
clazz.toString = () => `class ${clazz.name}#${name}` | ||
if (type === "class") { | ||
;(clazz as any)[modelTypeKey] = name | ||
} | ||
// this also gives access to modelInitializersSymbol, modelPropertiesSymbol, modelDataTypeCheckerSymbol | ||
Object.setPrototypeOf(newClazz, clazz) | ||
newClazz.prototype = clazz.prototype | ||
// this also gives access to modelInitializersSymbol, modelPropertiesSymbol, modelDataTypeCheckerSymbol | ||
Object.setPrototypeOf(newClazz, clazz) | ||
newClazz.prototype = clazz.prototype | ||
Object.defineProperty(newClazz, "name", { | ||
...Object.getOwnPropertyDescriptor(newClazz, "name"), | ||
value: clazz.name, | ||
}) | ||
newClazz[modelUnwrappedClassSymbol] = clazz | ||
Object.defineProperty(newClazz, "name", { | ||
...Object.getOwnPropertyDescriptor(newClazz, "name"), | ||
value: clazz.name, | ||
}) | ||
newClazz[modelUnwrappedClassSymbol] = clazz | ||
const modelInfo = { | ||
name, | ||
class: newClazz, | ||
} | ||
const modelInfo = { | ||
name, | ||
class: newClazz, | ||
} | ||
modelInfoByName[name] = modelInfo | ||
modelInfoByName[name] = modelInfo | ||
modelInfoByClass.set(newClazz, modelInfo) | ||
modelInfoByClass.set(clazz, modelInfo) | ||
modelInfoByClass.set(newClazz, modelInfo) | ||
modelInfoByClass.set(clazz, modelInfo) | ||
runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol) | ||
runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol) | ||
return newClazz | ||
} | ||
return newClazz | ||
} | ||
// basically taken from TS | ||
@@ -142,0 +143,0 @@ function tsDecorate(decorators: any, target: any, key: any, desc: any) { |
import type { O } from "ts-toolbelt" | ||
import type { LateTypeChecker, TypeChecker } from "../typeChecking/TypeChecker" | ||
import { getOrCreate } from "../utils/mapUtils" | ||
import type { IsOptionalValue } from "../utils/types" | ||
@@ -16,2 +17,4 @@ | ||
TPropCreationValue, | ||
TTransformedValue, | ||
TTransformedCreationValue, | ||
TIsOptional, | ||
@@ -23,2 +26,4 @@ TIsId extends boolean = false, | ||
$creationValueType: TPropCreationValue | ||
$transformedValueType: TTransformedValue | ||
$transformedCreationValueType: TTransformedCreationValue | ||
$isOptional: TIsOptional | ||
@@ -33,14 +38,75 @@ $isId: TIsId | ||
isId: boolean | ||
transform: | ||
| { | ||
transform( | ||
original: unknown, | ||
model: object, | ||
propName: PropertyKey, | ||
setOriginalValue: (newOriginalValue: unknown) => void | ||
): unknown | ||
untransform(transformed: unknown, model: object, propName: PropertyKey): unknown | ||
} | ||
| undefined | ||
withSetter(): ModelPropWithSetter<this> | ||
withSetter(): ModelProp< | ||
TPropValue, | ||
TPropCreationValue, | ||
TTransformedValue, | ||
TTransformedCreationValue, | ||
TIsOptional, | ||
TIsId, | ||
string | ||
> | ||
/** | ||
* @deprecated Setter methods are preferred. | ||
*/ | ||
withSetter(mode: "assign"): ModelPropWithSetter<this> | ||
withSetter( | ||
mode: "assign" | ||
): ModelProp< | ||
TPropValue, | ||
TPropCreationValue, | ||
TTransformedValue, | ||
TTransformedCreationValue, | ||
TIsOptional, | ||
TIsId, | ||
string | ||
> | ||
/** | ||
* Sets a transform for the property. | ||
* | ||
* @typeparam TTV Transformed value type. | ||
* @param transform Transform to be used. | ||
* @returns | ||
*/ | ||
withTransform<TTV>( | ||
transform: ModelPropTransform<NonNullable<TPropValue>, TTV> | ||
): ModelProp< | ||
TPropValue, | ||
TPropCreationValue, | ||
TTV | Extract<TPropValue, null | undefined>, | ||
TTV | Extract<TPropCreationValue, null | undefined>, | ||
TIsOptional, | ||
TIsId, | ||
THasSetter | ||
> | ||
} | ||
/** | ||
* A model prop transform. | ||
*/ | ||
export interface ModelPropTransform<TOriginal, TTransformed> { | ||
transform(params: { | ||
originalValue: TOriginal | ||
cachedTransformedValue: TTransformed | undefined | ||
setOriginalValue(value: TOriginal): void | ||
}): TTransformed | ||
untransform(params: { transformedValue: TTransformed; cacheTransformedValue(): void }): TOriginal | ||
} | ||
/** | ||
* Any model property. | ||
*/ | ||
export type AnyModelProp = ModelProp<any, any, any, any, any> | ||
export type AnyModelProp = ModelProp<any, any, any, any, any, any, any> | ||
@@ -75,5 +141,22 @@ /** | ||
export type ModelPropsToTransformedData<MP extends ModelProps> = { | ||
[k in keyof MP]: MP[k]["$transformedValueType"] | ||
} | ||
// we don't use O.Optional anymore since it generates unions too heavy | ||
// also if we use pick over the optional props we will loose the ability | ||
// to infer generics | ||
export type ModelPropsToTransformedCreationData<MP extends ModelProps> = { | ||
[k in keyof MP]?: MP[k]["$transformedCreationValueType"] | ||
} & | ||
O.Omit< | ||
{ | ||
[k in keyof MP]: MP[k]["$transformedCreationValueType"] | ||
}, | ||
OptionalModelProps<MP> | ||
> | ||
export type ModelPropsToSetter<MP extends ModelProps> = { | ||
[k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize<k & string>}`]: ( | ||
value: MP[k]["$valueType"] | ||
value: MP[k]["$transformedValueType"] | ||
) => void | ||
@@ -93,3 +176,3 @@ } | ||
}, | ||
} as any as ModelProp<string, string, string, true> | ||
} as any as ModelProp<string, string, string, string, string, true> | ||
@@ -107,2 +190,4 @@ /** | ||
TPropValue, | ||
TPropValue, | ||
TPropValue, | ||
IsOptionalValue<TPropValue, string, never> | ||
@@ -117,2 +202,4 @@ > | ||
TPropValue | null | undefined, | ||
TPropValue, | ||
TPropValue | null | undefined, | ||
string | ||
@@ -122,9 +209,2 @@ > | ||
/** | ||
* A model prop with a generated setter. | ||
*/ | ||
export type ModelPropWithSetter<MP extends AnyModelProp> = Omit<MP, "$hasSetter"> & { | ||
$hasSetter: string | ||
} | ||
/** | ||
* Defines a model property, with an optional function to generate a default value | ||
@@ -177,3 +257,3 @@ * if the input snapshot / model creation data is `null` or `undefined`. | ||
// base | ||
export function prop<TValue>(def?: any): ModelProp<TValue, any, any, any, any> { | ||
export function prop(def?: any): AnyModelProp { | ||
let hasDefaultValue = false | ||
@@ -188,5 +268,7 @@ | ||
const obj: ReturnType<typeof prop> = { | ||
const obj: AnyModelProp = { | ||
$valueType: null as any, | ||
$creationValueType: null as any, | ||
$transformedValueType: null as any, | ||
$transformedCreationValueType: null as any, | ||
$isOptional: null as any, | ||
@@ -201,2 +283,3 @@ $isId: null as never, | ||
isId: false, | ||
transform: undefined, | ||
@@ -206,4 +289,78 @@ withSetter(mode?: boolean | "assign") { | ||
}, | ||
withTransform(transform: ModelPropTransform<unknown, unknown>) { | ||
return { ...this, transform: toFullTransform(transform) } | ||
}, | ||
} | ||
return obj as any | ||
return obj | ||
} | ||
let cacheTransformResult = false | ||
const cacheTransformedValueFn = () => { | ||
cacheTransformResult = true | ||
} | ||
function toFullTransform(transformObject: ModelPropTransform<unknown, unknown>) { | ||
const cache = new WeakMap< | ||
object, | ||
Map<PropertyKey, { originalValue: unknown; transformedValue: unknown }> | ||
>() | ||
const transform = (params: { | ||
originalValue: unknown | ||
cachedTransformedValue: unknown | ||
setOriginalValue(newOriginalValue: unknown): void | ||
}) => (params.originalValue == null ? params.originalValue : transformObject.transform(params)) | ||
const untransform = (params: { transformedValue: unknown; cacheTransformedValue(): void }) => | ||
params.transformedValue == null ? params.transformedValue : transformObject.untransform(params) | ||
return { | ||
transform( | ||
originalValue: unknown, | ||
model: object, | ||
propName: PropertyKey, | ||
setOriginalValue: (newOriginalValue: unknown) => void | ||
) { | ||
const modelCache = getOrCreate(cache, model, () => new Map()) | ||
let propCache = modelCache.get(propName) | ||
if (propCache?.originalValue !== originalValue) { | ||
// original changed, invalidate cache | ||
modelCache.delete(propName) | ||
propCache = undefined | ||
} | ||
const transformedValue = transform({ | ||
originalValue, | ||
cachedTransformedValue: propCache?.transformedValue, | ||
setOriginalValue, | ||
}) | ||
modelCache.set(propName, { | ||
originalValue, | ||
transformedValue, | ||
}) | ||
return transformedValue | ||
}, | ||
untransform(transformedValue: unknown, model: object, propName: PropertyKey) { | ||
const modelCache = getOrCreate(cache, model, () => new Map()) | ||
cacheTransformResult = false | ||
const originalValue = untransform({ | ||
transformedValue, | ||
cacheTransformedValue: cacheTransformedValueFn, | ||
}) | ||
if (cacheTransformResult) { | ||
modelCache.set(propName, { originalValue, transformedValue }) | ||
} else { | ||
modelCache.delete(propName) | ||
} | ||
return originalValue | ||
}, | ||
} | ||
} |
@@ -0,0 +0,0 @@ import { assertTweakedObject } from "../tweaker/core" |
type AnyMap<V = any> = Map<any, V> | WeakMap<any, V> | ||
export function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V | ||
export function getOrCreate<K extends object, V>(map: WeakMap<K, V>, key: K, create: () => V): V | ||
export function getOrCreate<K, V, C = V>(map: Map<K, V>, key: K, create: () => C): V | ||
export function getOrCreate<K extends object, V, C = V>( | ||
map: WeakMap<K, V>, | ||
key: K, | ||
create: () => C | ||
): V | ||
@@ -6,0 +10,0 @@ export function getOrCreate<V>(map: AnyMap<V>, key: any, create: () => V) { |
@@ -23,74 +23,96 @@ import { | ||
} from "../utils" | ||
import { Lock } from "../utils/lock" | ||
import { tag } from "../utils/tag" | ||
const observableMapBackedByObservableObject = action(<T>(obj: object): ObservableMap<string, T> & { | ||
dataObject: typeof obj | ||
} => { | ||
if (inDevMode()) { | ||
if (!isObservableObject(obj)) { | ||
throw failure("assertion failed: expected an observable object") | ||
const observableMapBackedByObservableObject = action( | ||
<T>( | ||
obj: object | ||
): ObservableMap<string, T> & { | ||
dataObject: typeof obj | ||
} => { | ||
if (inDevMode()) { | ||
if (!isObservableObject(obj)) { | ||
throw failure("assertion failed: expected an observable object") | ||
} | ||
} | ||
} | ||
const map = observable.map() | ||
;(map as any).dataObject = obj | ||
const map = observable.map() | ||
;(map as any).dataObject = obj | ||
const keys = Object.keys(obj) | ||
for (let i = 0; i < keys.length; i++) { | ||
const k = keys[i] | ||
map.set(k, (obj as any)[k]) | ||
} | ||
const keys = Object.keys(obj) | ||
for (let i = 0; i < keys.length; i++) { | ||
const k = keys[i] | ||
map.set(k, (obj as any)[k]) | ||
} | ||
const mutationLock = new Lock() | ||
let mapAlreadyChanged = false | ||
let objectAlreadyChanged = false | ||
// when the object changes the map changes | ||
observe( | ||
obj, | ||
action( | ||
mutationLock.unlockedFn((change: IObjectDidChange) => { | ||
switch (change.type) { | ||
case "add": | ||
case "update": { | ||
map.set(change.name, change.newValue) | ||
break | ||
} | ||
// when the object changes the map changes | ||
observe( | ||
obj, | ||
action((change: IObjectDidChange) => { | ||
if (mapAlreadyChanged) { | ||
return | ||
} | ||
case "remove": { | ||
map.delete(change.name) | ||
break | ||
objectAlreadyChanged = true | ||
try { | ||
switch (change.type) { | ||
case "add": | ||
case "update": { | ||
map.set(change.name, change.newValue) | ||
break | ||
} | ||
case "remove": { | ||
map.delete(change.name) | ||
break | ||
} | ||
} | ||
} finally { | ||
objectAlreadyChanged = false | ||
} | ||
}) | ||
) | ||
) | ||
// when the map changes also change the object | ||
intercept( | ||
map, | ||
action((change: IMapWillChange<string, T>) => { | ||
if (!mutationLock.isLocked) { | ||
return null // already changed | ||
} | ||
// when the map changes also change the object | ||
intercept( | ||
map, | ||
action((change: IMapWillChange<string, T>) => { | ||
if (mapAlreadyChanged) { | ||
return null | ||
} | ||
switch (change.type) { | ||
case "add": | ||
case "update": { | ||
set(obj, change.name, change.newValue) | ||
break | ||
if (objectAlreadyChanged) { | ||
return change | ||
} | ||
case "delete": { | ||
remove(obj, change.name) | ||
break | ||
mapAlreadyChanged = true | ||
try { | ||
switch (change.type) { | ||
case "add": | ||
case "update": { | ||
set(obj, change.name, change.newValue) | ||
break | ||
} | ||
case "delete": { | ||
remove(obj, change.name) | ||
break | ||
} | ||
} | ||
return change | ||
} finally { | ||
mapAlreadyChanged = false | ||
} | ||
} | ||
}) | ||
) | ||
return change | ||
}) | ||
) | ||
return map as any | ||
} | ||
) | ||
return map as any | ||
}) | ||
const observableMapBackedByObservableArray = action( | ||
@@ -113,3 +135,4 @@ <T>( | ||
const mutationLock = new Lock() | ||
let mapAlreadyChanged = false | ||
let arrayAlreadyChanged = false | ||
@@ -122,4 +145,9 @@ // for speed reasons we will just assume distinct values are only once in the array | ||
array, | ||
action( | ||
mutationLock.unlockedFn((change: any /*IArrayDidChange<[string, T]>*/) => { | ||
action((change: any /*IArrayDidChange<[string, T]>*/) => { | ||
if (mapAlreadyChanged) { | ||
return | ||
} | ||
arrayAlreadyChanged = true | ||
try { | ||
switch (change.type) { | ||
@@ -150,4 +178,6 @@ case "splice": { | ||
} | ||
}) | ||
) | ||
} finally { | ||
arrayAlreadyChanged = false | ||
} | ||
}) | ||
) | ||
@@ -159,29 +189,39 @@ | ||
action((change: IMapWillChange<string, T>) => { | ||
if (!mutationLock.isLocked) { | ||
return null // already changed | ||
if (mapAlreadyChanged) { | ||
return null | ||
} | ||
switch (change.type) { | ||
case "update": { | ||
// replace the whole tuple to keep tuple immutability | ||
const i = array.findIndex((i) => i[0] === change.name) | ||
array[i] = [change.name, change.newValue!] | ||
break | ||
} | ||
if (arrayAlreadyChanged) { | ||
return change | ||
} | ||
case "add": { | ||
array.push([change.name, change.newValue!]) | ||
break | ||
} | ||
mapAlreadyChanged = true | ||
case "delete": { | ||
const i = array.findIndex((i) => i[0] === change.name) | ||
if (i >= 0) { | ||
array.splice(i, 1) | ||
try { | ||
switch (change.type) { | ||
case "update": { | ||
// replace the whole tuple to keep tuple immutability | ||
const i = array.findIndex((i) => i[0] === change.name) | ||
array[i] = [change.name, change.newValue!] | ||
break | ||
} | ||
break | ||
case "add": { | ||
array.push([change.name, change.newValue!]) | ||
break | ||
} | ||
case "delete": { | ||
const i = array.findIndex((i) => i[0] === change.name) | ||
if (i >= 0) { | ||
array.splice(i, 1) | ||
} | ||
break | ||
} | ||
} | ||
return change | ||
} finally { | ||
mapAlreadyChanged = false | ||
} | ||
return change | ||
}) | ||
@@ -209,5 +249,5 @@ ) | ||
*/ | ||
export function asMap<T>( | ||
array: Array<[string, T]> | ||
): ObservableMap<string, T> & { dataObject: Array<[string, T]> } | ||
export function asMap<K, V>( | ||
array: Array<[K, V]> | ||
): ObservableMap<K, V> & { dataObject: Array<[K, V]> } | ||
@@ -228,5 +268,5 @@ /** | ||
*/ | ||
export function asMap<T>( | ||
objOrArray: Record<string, T> | Array<[string, T]> | ||
): ObservableMap<string, T> & { dataObject: typeof objOrArray } { | ||
export function asMap( | ||
objOrArray: Record<string, unknown> | Array<[unknown, unknown]> | ||
): ObservableMap<unknown, unknown> & { dataObject: typeof objOrArray } { | ||
return asMapTag.for(objOrArray) as any | ||
@@ -261,3 +301,3 @@ } | ||
*/ | ||
export function mapToArray<T>(map: Map<string, T>): Array<[string, T]> { | ||
export function mapToArray<K, V>(map: Map<K, V>): Array<[K, V]> { | ||
assertIsMap(map, "map") | ||
@@ -270,5 +310,5 @@ | ||
const arr: [string, any][] = [] | ||
const arr: [K, V][] = [] | ||
for (const k of map.keys()) { | ||
arr.push([k, map.get(k)]) | ||
arr.push([k, map.get(k)!]) | ||
} | ||
@@ -275,0 +315,0 @@ |
@@ -12,3 +12,2 @@ import { | ||
import { assertIsObservableArray, assertIsSet, failure, inDevMode } from "../utils" | ||
import { Lock } from "../utils/lock" | ||
import { tag } from "../utils/tag" | ||
@@ -31,3 +30,4 @@ | ||
const mutationLock = new Lock() | ||
let setAlreadyChanged = false | ||
let arrayAlreadyChanged = false | ||
@@ -39,4 +39,10 @@ // for speed reasons we will just assume distinct values are only once in the array | ||
array, | ||
action( | ||
mutationLock.unlockedFn((change: any /*IArrayDidChange<T>*/) => { | ||
action((change: any /*IArrayDidChange<T>*/) => { | ||
if (setAlreadyChanged) { | ||
return | ||
} | ||
arrayAlreadyChanged = true | ||
try { | ||
switch (change.type) { | ||
@@ -67,4 +73,6 @@ case "splice": { | ||
} | ||
}) | ||
) | ||
} finally { | ||
arrayAlreadyChanged = false | ||
} | ||
}) | ||
) | ||
@@ -76,22 +84,32 @@ | ||
action((change: ISetWillChange<T>) => { | ||
if (!mutationLock.isLocked) { | ||
return null // already changed | ||
if (setAlreadyChanged) { | ||
return null | ||
} | ||
switch (change.type) { | ||
case "add": { | ||
array.push(change.newValue) | ||
break | ||
} | ||
if (arrayAlreadyChanged) { | ||
return change | ||
} | ||
case "delete": { | ||
const i = array.indexOf(change.oldValue) | ||
if (i >= 0) { | ||
array.splice(i, 1) | ||
setAlreadyChanged = true | ||
try { | ||
switch (change.type) { | ||
case "add": { | ||
array.push(change.newValue) | ||
break | ||
} | ||
break | ||
case "delete": { | ||
const i = array.indexOf(change.oldValue) | ||
if (i >= 0) { | ||
array.splice(i, 1) | ||
} | ||
break | ||
} | ||
} | ||
return change | ||
} finally { | ||
setAlreadyChanged = false | ||
} | ||
return change | ||
}) | ||
@@ -98,0 +116,0 @@ ) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1791437
365
41697