Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit prop transform #124

Merged
merged 4 commits into from
Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change Log

- Property transforms can now be used standalone.
- [BREAKING CHANGE - types] Some type helpers have been renamed: `ModelData` -> `ModelPropsData` / `ModelInstanceData`, `ModelCreationData` -> `ModelPropsCreationData` / `ModelInstanceCreationData`.
- New feature: "Implicit property transforms", which are sometimes preferred over the old decorator based property transforms, collection wrappers (`arrayAsSet`, `arrayAsMap`, `objectAsMap`) and collection models (`ArraySet`, `ObjectMap`). Check the "Maps, Sets, Dates" section in the docs for more info.
- Added `types.tuple`.
- Property transforms decorators can now also be used standalone.

## 0.39.0

Expand Down
89 changes: 74 additions & 15 deletions packages/lib/src/model/BaseModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { observable } from "mobx"
import { O } from "ts-toolbelt"
import { getGlobalConfig } from "../globalConfig"
import { PropTransform } from "../propTransform"
import { memoTransformCache } from "../propTransform/propTransform"
import { getSnapshot } from "../snapshot/getSnapshot"
import { SnapshotInOfModel, SnapshotInOfObject, SnapshotOutOfModel } from "../snapshot/SnapshotOf"
import { typesModel } from "../typeChecking/model"
Expand All @@ -12,8 +14,10 @@ import { modelInfoByClass } from "./modelInfo"
import { internalNewModel } from "./newModel"
import { assertIsModelClass } from "./utils"

declare const dataTypeSymbol: unique symbol
declare const creationDataTypeSymbol: unique symbol
declare const propsDataTypeSymbol: unique symbol
declare const propsCreationDataTypeSymbol: unique symbol
declare const instanceDataTypeSymbol: unique symbol
declare const instanceCreationDataTypeSymbol: unique symbol

/**
* @ignore
Expand All @@ -30,12 +34,16 @@ export const modelInitializedSymbol = Symbol("modelInitialized")
* @typeparam CreationData Creation data type.
*/
export abstract class BaseModel<
Data extends { [k: string]: any },
CreationData extends { [k: string]: any }
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
[dataTypeSymbol]: Data;
[creationDataTypeSymbol]: CreationData;
[propsDataTypeSymbol]: PropsData;
[propsCreationDataTypeSymbol]: PropsCreationData;
[instanceDataTypeSymbol]: InstanceData;
[instanceCreationDataTypeSymbol]: InstanceCreationData;

/**
* Model type name.
Expand Down Expand Up @@ -63,8 +71,9 @@ export abstract class BaseModel<
/**
* 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 $!: Data
readonly $!: PropsData

/**
* Optional hook that will run once this model instance is attached to the tree of a model marked as
Expand All @@ -89,7 +98,7 @@ export abstract class BaseModel<
*/
fromSnapshot?(snapshot: {
[k: string]: any
}): SnapshotInOfObject<CreationData> & { [modelTypeKey]?: string; [modelIdKey]?: string }
}): SnapshotInOfObject<PropsCreationData> & { [modelTypeKey]?: string; [modelIdKey]?: string }

/**
* Performs a type check over the model instance.
Expand All @@ -105,17 +114,36 @@ export abstract class BaseModel<
/**
* Creates an instance of Model.
*/
constructor(data: CreationData & { [modelIdKey]?: string }) {
const initialData: any = data
constructor(data: InstanceCreationData & { [modelIdKey]?: string }) {
let initialData: any = data
const snapshotInitialData: any = arguments[1]
const clazz: ModelClass<AnyModel> = arguments[2]
const propsWithTransforms: [string, PropTransform<any, any>][] = arguments[4]

Object.setPrototypeOf(this, clazz.prototype)

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], undefined)
}
}

internalNewModel(
this,
clazz,
Expand Down Expand Up @@ -168,7 +196,7 @@ export interface AnyModel extends BaseModel<any, any> {}
* Extracts the instance type of a model class.
*/
export interface ModelClass<M extends AnyModel> {
new (initialData: ModelCreationData<M> & { [modelIdKey]?: string }): M
new (initialData: ModelInstanceCreationData<M> & { [modelIdKey]?: string }): M
}

/**
Expand Down Expand Up @@ -205,14 +233,45 @@ export function modelClass<T extends AnyModel>(type: { prototype: T }): ModelCla
}

/**
* The data type of a model.
* 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 extends BaseModel<
any,
infer PropsCreationData,
any,
any
>
? PropsCreationData
: never

/**
* The instance data type of a model.
*/
export type ModelData<M extends AnyModel> = M["$"]
export type ModelInstanceData<M extends AnyModel> = M extends BaseModel<
any,
any,
infer InstanceData,
any
>
? InstanceData
: never

/**
* The creation data type of a model.
* The transformed creation data type of a model.
*/
export type ModelCreationData<M extends AnyModel> = M extends BaseModel<any, infer C> ? C : never
export type ModelInstanceCreationData<M extends AnyModel> = M extends BaseModel<
any,
any,
any,
infer InstanceCreationData
>
? InstanceCreationData
: never

/**
* Add missing model metadata to a model creation snapshot to generate a proper model snapshot.
Expand Down
145 changes: 106 additions & 39 deletions packages/lib/src/model/Model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { O } from "ts-toolbelt"
import { memoTransformCache, PropTransform } from "../propTransform/propTransform"
import { typesObject } from "../typeChecking/object"
import { LateTypeChecker } from "../typeChecking/TypeChecker"
import { typesUnchecked } from "../typeChecking/unchecked"
Expand All @@ -17,38 +18,58 @@ import {
modelPropertiesSymbol,
modelUnwrappedClassSymbol,
} from "./modelSymbols"
import { ModelProps, ModelPropsToCreationData, ModelPropsToData, OptionalModelProps } from "./prop"
import {
ModelProps,
ModelPropsToInstanceCreationData,
ModelPropsToInstanceData,
ModelPropsToPropsCreationData,
ModelPropsToPropsData,
OptionalModelProps,
} from "./prop"
import { assertIsModelClass } from "./utils"

declare const propsDataSymbol: unique symbol
declare const creationPropsDataSymbol: unique symbol
declare const optPropsDataSymbol: unique symbol
declare const creationDataSymbol: unique symbol
declare const composedCreationDataSymbol: unique symbol
declare const instanceDataSymbol: unique symbol

declare const optDataSymbol: unique symbol

declare const propsCreationDataSymbol: unique symbol
declare const instanceCreationDataSymbol: unique symbol

declare const composedPropsCreationDataSymbol: unique symbol
declare const composedInstanceCreationDataSymbol: unique symbol

export interface _Model<SuperModel, TProps extends ModelProps> {
/**
* Model type name assigned to this class, or undefined if none.
*/
readonly [modelTypeKey]: string | undefined

[propsDataSymbol]: ModelPropsToData<TProps>
[creationPropsDataSymbol]: ModelPropsToCreationData<TProps>
[propsDataSymbol]: ModelPropsToPropsData<TProps>
[instanceDataSymbol]: ModelPropsToInstanceData<TProps>

[optPropsDataSymbol]: OptionalModelProps<TProps>
[optDataSymbol]: OptionalModelProps<TProps>

[creationDataSymbol]: O.Optional<
this[typeof creationPropsDataSymbol],
this[typeof optPropsDataSymbol]
>
[propsCreationDataSymbol]: ModelPropsToPropsCreationData<TProps>
[instanceCreationDataSymbol]: ModelPropsToInstanceCreationData<TProps>

[composedCreationDataSymbol]: SuperModel extends BaseModel<any, infer CD>
? O.Merge<CD, this[typeof creationDataSymbol]>
: this[typeof creationDataSymbol]
[composedPropsCreationDataSymbol]: SuperModel extends BaseModel<any, infer CD>
? O.Merge<CD, this[typeof propsCreationDataSymbol]>
: this[typeof propsCreationDataSymbol]
[composedInstanceCreationDataSymbol]: SuperModel extends BaseModel<any, infer CD>
? O.Merge<CD, this[typeof instanceCreationDataSymbol]>
: this[typeof instanceCreationDataSymbol]

new (data: this[typeof composedCreationDataSymbol] & { [modelIdKey]?: string }): SuperModel &
BaseModel<this[typeof propsDataSymbol], this[typeof composedCreationDataSymbol]> &
Omit<this[typeof propsDataSymbol], keyof AnyModel>
new (
data: this[typeof composedInstanceCreationDataSymbol] & { [modelIdKey]?: string }
): SuperModel &
BaseModel<
this[typeof propsDataSymbol],
this[typeof composedPropsCreationDataSymbol],
this[typeof instanceDataSymbol],
this[typeof composedInstanceCreationDataSymbol]
> &
Omit<this[typeof instanceDataSymbol], keyof AnyModel>
}

/**
Expand Down Expand Up @@ -111,7 +132,7 @@ function internalModel<TProps extends ModelProps, TBaseModel extends AnyModel>(
}
} else {
// define $modelId on the base
extraDescriptors[modelIdKey] = createModelPropDescriptor(modelIdKey, true)
extraDescriptors[modelIdKey] = createModelPropDescriptor(modelIdKey, undefined, true)
}

// create type checker if needed
Expand All @@ -131,11 +152,19 @@ function internalModel<TProps extends ModelProps, TBaseModel extends AnyModel>(
for (const modelPropName of Object.keys(modelProps).filter(
mp => !baseModelPropNames.has(mp as any)
)) {
extraDescriptors[modelPropName] = createModelPropDescriptor(modelPropName, false)
extraDescriptors[modelPropName] = createModelPropDescriptor(
modelPropName,
modelProps[modelPropName].transform,
false
)
}

const base: any = baseModel || BaseModel

const propsWithTransform = Object.entries(modelProps)
.filter(([_propName, prop]) => !!prop.transform)
.map(([propName, prop]) => [propName, prop.transform!] as const)

// we use this weird hack rather than just class CustomBaseModel extends base {}
// in order to work around problems with ES5 classes extending ES6 classes
// see https://github.com/xaviergonz/mobx-keystone/issues/15
Expand All @@ -153,7 +182,8 @@ function internalModel<TProps extends ModelProps, TBaseModel extends AnyModel>(
initialData,
snapshotInitialData,
modelConstructor || this.constructor,
generateNewIds
generateNewIds,
propsWithTransform
)
}

Expand All @@ -179,23 +209,60 @@ function _inheritsLoose(subClass: any, superClass: any) {
subClass.__proto__ = superClass
}

function createModelPropDescriptor(modelPropName: string, enumerable: boolean): PropertyDescriptor {
return {
enumerable,
configurable: true,
get(this: AnyModel) {
// no need to use get since these vars always get on the initial $
return this.$[modelPropName]
},
set(this: AnyModel, v?: any) {
// hack to only permit setting these values once fully constructed
// this is to ignore abstract properties being set by babel
// see https://github.com/xaviergonz/mobx-keystone/issues/18
if (!(this as any)[modelInitializedSymbol]) {
return
}
// no need to use set since these vars always get on the initial $
this.$[modelPropName] = v
},
function createModelPropDescriptor(
modelPropName: string,
transform: PropTransform<any, any> | undefined,
enumerable: boolean
): PropertyDescriptor {
// the code is duplicated to ensure better speed
if (transform) {
return {
enumerable,
configurable: true,
get(this: AnyModel) {
// no need to use get since these vars always get on the initial $
const memoTransform = memoTransformCache.getOrCreateMemoTransform(
this,
modelPropName,
transform
)
return memoTransform.propToData(this.$[modelPropName])
},
set(this: AnyModel, v?: any) {
// hack to only permit setting these values once fully constructed
// this is to ignore abstract properties being set by babel
// see https://github.com/xaviergonz/mobx-keystone/issues/18
if (!(this as any)[modelInitializedSymbol]) {
return
}
// no need to use set since these vars always get on the initial $
const memoTransform = memoTransformCache.getOrCreateMemoTransform(
this,
modelPropName,
transform
)
const oldPropValue = this.$[modelPropName]
this.$[modelPropName] = memoTransform.dataToProp(v, oldPropValue)
},
}
} else {
return {
enumerable,
configurable: true,
get(this: AnyModel) {
// no need to use get since these vars always get on the initial $
return this.$[modelPropName]
},
set(this: AnyModel, v?: any) {
// hack to only permit setting these values once fully constructed
// this is to ignore abstract properties being set by babel
// see https://github.com/xaviergonz/mobx-keystone/issues/18
if (!(this as any)[modelInitializedSymbol]) {
return
}
// no need to use set since these vars always get on the initial $
this.$[modelPropName] = v
},
}
}
}
4 changes: 2 additions & 2 deletions packages/lib/src/model/newModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getGlobalConfig, isModelAutoTypeCheckingEnabled } from "../globalConfig
import { tweakModel } from "../tweaker/tweakModel"
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
import { failure, inDevMode, makePropReadonly } from "../utils"
import { AnyModel, ModelClass, ModelCreationData } from "./BaseModel"
import { AnyModel, ModelClass, ModelPropsCreationData } from "./BaseModel"
import { getModelDataType } from "./getModelDataType"
import { modelIdKey, modelTypeKey } from "./metadata"
import { modelInfoByClass } from "./modelInfo"
Expand All @@ -20,7 +20,7 @@ export const internalNewModel = action(
<M extends AnyModel>(
origModelObj: M,
modelClass: ModelClass<M>,
initialData: (ModelCreationData<M> & { [modelIdKey]?: string }) | undefined,
initialData: (ModelPropsCreationData<M> & { [modelIdKey]?: string }) | undefined,
snapshotInitialData:
| {
unprocessedSnapshot: any
Expand Down
Loading