UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

271 lines (238 loc) 8.3 kB
import { observable } from "mobx" import { fromSnapshotOverrideTypeSymbol, ModelClass, propsTypeSymbol, toSnapshotOverrideTypeSymbol, } from "../modelShared/BaseModelShared" import { modelInfoByClass } from "../modelShared/modelInfo" import type { ModelProps, ModelPropsToTransformedCreationData, ModelPropsToUntransformedData, } from "../modelShared/prop" import { getSnapshot } from "../snapshot/getSnapshot" import type { SnapshotInOfModel, SnapshotOutOfModel } from "../snapshot/SnapshotOf" import { typesModel } from "../types/objectBased/typesModel" import { typeCheck } from "../types/typeCheck" import type { TypeCheckError } from "../types/TypeCheckError" import { assertIsObject, failure } from "../utils" import { getModelIdPropertyName } from "./getModelMetadata" import { modelIdKey, modelTypeKey } from "./metadata" import type { ModelConstructorOptions } from "./ModelConstructorOptions" import { internalFromSnapshotModel, internalNewModel } from "./newModel" import { assertIsModelClass } from "./utils" /** * @ignore */ export const modelIdPropertyNameSymbol = Symbol("modelIdPropertyName") /** * @ignore */ export type ModelIdPropertyType<TProps extends ModelProps, ModelIdPropertyName extends string> = [ ModelIdPropertyName, ] extends [never] ? never : ModelPropsToUntransformedData<Pick<TProps, ModelIdPropertyName>>[ModelIdPropertyName] /** * Base abstract class for models. Use `Model` instead when extending. * * Never override the constructor, use `onInit` or `onAttachedToRootStore` instead. * * @template Data Data type. * @template CreationData Creation data type. * @template ModelIdPropertyName Model id property name. */ export abstract class BaseModel< TProps extends ModelProps, FromSnapshotOverride extends Record<string, any>, ToSnapshotOverride extends Record<string, any>, ModelIdPropertyName extends string = never, > { // just to make typing work properly [propsTypeSymbol]!: TProps; [fromSnapshotOverrideTypeSymbol]!: FromSnapshotOverride; [toSnapshotOverrideTypeSymbol]!: ToSnapshotOverride; [modelIdPropertyNameSymbol]!: ModelIdPropertyName /** * Model type name. */ readonly [modelTypeKey]!: string /** * Model internal id. Can be modified inside a model action. * It will return `undefined` if there's no id prop set. */ get [modelIdKey](): ModelIdPropertyType<TProps, ModelIdPropertyName> { const idProp = getModelIdPropertyName(this.constructor as any) return idProp ? this.$[idProp] : (undefined as any) } set [modelIdKey](newId: ModelIdPropertyType<TProps, ModelIdPropertyName>) { const idProp = getModelIdPropertyName(this.constructor as any) if (!idProp) { throw failure("$modelId cannot be set when there is no idProp set in the model") } ;(this.$ as any)[idProp] = newId } /** * Can be overridden to offer a reference id to be used in reference resolution. * By default it will use the `idProp` if available or return `undefined` otherwise. */ getRefId(): string | undefined { return this[modelIdKey] } /** * Called after the model has been created. */ protected onInit?(): void /** * 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. */ readonly $!: ModelPropsToUntransformedData<TProps> /** * Optional hook that will run once this model instance is attached to the tree of a model marked as * root store via `registerRootStore`. * Basically this is the place where you know the full root store is complete and where things such as * middlewares, effects (reactions, etc), and other side effects should be registered, since it means * that the model is now part of the active application state. * * It can return a disposer that will be run once this model instance is detached from such root store tree. * * @param rootStore * @returns */ protected onAttachedToRootStore?(rootStore: object): (() => void) | void /** * Performs a type check over the model instance. * For this to work a data type has to be declared as part of the model properties. * * @returns A `TypeCheckError` or `null` if there is no error. */ typeCheck(): TypeCheckError | null { const type = typesModel<this>(this.constructor as any) return typeCheck(type, this as any) } /** * Creates an instance of a model. */ constructor(data: ModelPropsToTransformedCreationData<TProps>) { const initialData = data as any const { snapshotInitialData, modelClass, generateNewIds }: ModelConstructorOptions = arguments[1] Object.setPrototypeOf(this, modelClass!.prototype) const self = this as any // delete unnecessary props delete self[propsTypeSymbol] delete self[fromSnapshotOverrideTypeSymbol] delete self[toSnapshotOverrideTypeSymbol] delete self[modelIdPropertyNameSymbol] if (snapshotInitialData) { // from snapshot internalFromSnapshotModel(this, snapshotInitialData, modelClass!, !!generateNewIds) } else { // plain new assertIsObject(initialData, "initialData") internalNewModel( this, observable.object(initialData as any, undefined, { deep: false }), modelClass! ) } } toString(options?: { withData?: boolean }) { const finalOptions = { withData: true, ...options, } const firstPart = `${this.constructor.name}#${this[modelTypeKey]}` return finalOptions.withData ? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]` : `[${firstPart}]` } } /** * @ignore */ export type BaseModelKeys = keyof AnyModel | "onInit" | "onAttachedToRootStore" // these props will never be hoisted to this (except for model id) /** * @internal */ export const baseModelPropNames = new Set<BaseModelKeys>([ modelTypeKey, modelIdKey, "onInit", "$", "getRefId", "onAttachedToRootStore", "typeCheck", ]) /** * Any kind of model instance. */ export interface AnyModel extends BaseModel<any, any, any, any> {} /** * @deprecated Should not be needed anymore. * * Tricks TypeScript into accepting abstract classes as a parameter for `ExtendedModel`. * Does nothing in runtime. * * @template T Abstract model class type. * @param type Abstract model class. * @returns */ export function abstractModelClass<T>(type: T): T & Object { return type as any } /** * The model id property name. */ export type ModelIdPropertyName<M extends AnyModel> = M[typeof modelIdPropertyNameSymbol] /** * Add missing model metadata to a model creation snapshot to generate a proper model snapshot. * Usually used alongside `fromSnapshot`. * * @template M Model type. * @param modelClass Model class. * @param snapshot Model creation snapshot without metadata. * @returns The model snapshot (including metadata). */ export function modelSnapshotInWithMetadata<M extends AnyModel>( modelClass: ModelClass<M>, snapshot: Omit<SnapshotInOfModel<M>, typeof modelTypeKey> ): SnapshotInOfModel<M> { assertIsModelClass(modelClass, "modelClass") assertIsObject(snapshot, "initialData") const modelInfo = modelInfoByClass.get(modelClass)! return { ...snapshot, [modelTypeKey]: modelInfo.name, } as any } /** * Add missing model metadata to a model output snapshot to generate a proper model snapshot. * Usually used alongside `applySnapshot`. * * @template M Model type. * @param modelClass Model class. * @param snapshot Model output snapshot without metadata. * @returns The model snapshot (including metadata). */ export function modelSnapshotOutWithMetadata<M extends AnyModel>( modelClass: ModelClass<M>, snapshot: Omit<SnapshotOutOfModel<M>, typeof modelTypeKey> ): SnapshotOutOfModel<M> { assertIsModelClass(modelClass, "modelClass") assertIsObject(snapshot, "initialData") const modelInfo = modelInfoByClass.get(modelClass)! return { ...snapshot, [modelTypeKey]: modelInfo.name, } as any } /** * A model class declaration, made of a base model and the model interface. */ export type ModelClassDeclaration<BaseModelClass, ModelInterface> = BaseModelClass & { new (...args: any[]): ModelInterface }