UNPKG

mobx-keystone-mindreframer

Version:

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

276 lines (242 loc) 8.52 kB
import { observable } from "mobx" import type { O } from "ts-toolbelt" import { getGlobalConfig } from "../globalConfig" import { creationDataTypeSymbol, dataTypeSymbol, ModelClass } from "../modelShared/BaseModelShared" import { modelInfoByClass } from "../modelShared/modelInfo" import { getSnapshot } from "../snapshot/getSnapshot" import type { SnapshotInOfModel, SnapshotInOfObject, SnapshotOutOfModel, } from "../snapshot/SnapshotOf" import { typesModel } from "../typeChecking/model" import { typeCheck } from "../typeChecking/typeCheck" import type { TypeCheckError } from "../typeChecking/TypeCheckError" import { assertIsObject } from "../utils" import { getModelIdPropertyName } from "./getModelMetadata" import { modelIdKey, modelTypeKey } from "./metadata" import type { ModelConstructorOptions } from "./ModelConstructorOptions" import { internalNewModel } from "./newModel" import { assertIsModelClass } from "./utils" import { setBaseModel } from "./_BaseModel" /** * @ignore */ export const modelIdPropertyNameSymbol = Symbol() /** * Base abstract class for models. Use `Model` instead when extending. * * Never override the constructor, use `onInit` or `onAttachedToRootStore` instead. * * @typeparam Data Data type. * @typeparam CreationData Creation data type. * @typeparam ModelIdPropertyName Model id property name. */ export abstract class BaseModel< Data extends { [k: string]: any }, CreationData extends { [k: string]: any }, ModelIdPropertyName extends string = typeof modelIdKey > { // just to make typing work properly [dataTypeSymbol]: Data; [creationDataTypeSymbol]: CreationData; [modelIdPropertyNameSymbol]: ModelIdPropertyName; /** * Model type name. */ readonly [modelTypeKey]: string /** * Model internal id. Can be modified inside a model action. */ get [modelIdKey](): string { return this.$[getModelIdPropertyName(this.constructor as any)] } set [modelIdKey](newId: string) { ;(this.$ as any)[getModelIdPropertyName(this.constructor as any)] = newId } /** * Can be overriden to offer a reference id to be used in reference resolution. * By default it will use the model id property (usually `$modelId` unless overridden). */ getRefId(): string { 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 $!: Data /** * 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 /** * Optional transformation that will be run when converting from a snapshot to the data part of the model. * Useful for example to do versioning and keep the data part up to date with the latest version of the model. * * @param snapshot The custom input snapshot. * @returns An input snapshot that must match the current model input snapshot. */ fromSnapshot?(snapshot: { [k: string]: any }): SnapshotInOfObject<CreationData> & { [modelTypeKey]?: string } /** * 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: CreationData) { let initialData = data as any const { snapshotInitialData, modelClass, generateNewIds, }: ModelConstructorOptions = arguments[1] as any Object.setPrototypeOf(this, modelClass!.prototype) const self = this as any // delete unnecessary props delete self[dataTypeSymbol] delete self[creationDataTypeSymbol] delete self[modelIdPropertyNameSymbol] if (!snapshotInitialData) { // plain new assertIsObject(initialData, "initialData") internalNewModel(this, observable.object(initialData as any, undefined, { deep: false }), { modelClass, generateNewIds: true, }) } else { // from snapshot internalNewModel(this, undefined, { modelClass, snapshotInitialData, generateNewIds }) } } 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}]` } } setBaseModel(BaseModel) /** * @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", "fromSnapshot", "typeCheck", ]) /** * Any kind of model instance. */ export interface AnyModel extends BaseModel<any, any, any> {} /** * @deprecated Should not be needed anymore. * * Tricks Typescript into accepting abstract classes as a parameter for `ExtendedModel`. * Does nothing in runtime. * * @typeparam 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`. * * @typeparam M Model type. * @param modelClass Model class. * @param snapshot Model creation snapshot without metadata. * @param [internalId] Model internal ID, or `undefined` to generate a new one. * @returns The model snapshot (including metadata). */ export function modelSnapshotInWithMetadata<M extends AnyModel>( modelClass: ModelClass<M>, snapshot: O.Omit<SnapshotInOfModel<M>, ModelIdPropertyName<M> | typeof modelTypeKey>, internalId: string = getGlobalConfig().modelIdGenerator() ): SnapshotInOfModel<M> { assertIsModelClass(modelClass, "modelClass") assertIsObject(snapshot, "initialData") const modelInfo = modelInfoByClass.get(modelClass)! const modelIdPropertyName = getModelIdPropertyName(modelClass) return { ...snapshot, [modelTypeKey]: modelInfo.name, [modelIdPropertyName]: internalId, } as any } /** * Add missing model metadata to a model output snapshot to generate a proper model snapshot. * Usually used alongside `applySnapshot`. * * @typeparam M Model type. * @param modelClass Model class. * @param snapshot Model output snapshot without metadata. * @param [internalId] Model internal ID, or `undefined` to generate a new one. * @returns The model snapshot (including metadata). */ export function modelSnapshotOutWithMetadata<M extends AnyModel>( modelClass: ModelClass<M>, snapshot: O.Omit<SnapshotOutOfModel<M>, ModelIdPropertyName<M> | typeof modelTypeKey>, internalId: string = getGlobalConfig().modelIdGenerator() ): SnapshotOutOfModel<M> { assertIsModelClass(modelClass, "modelClass") assertIsObject(snapshot, "initialData") const modelInfo = modelInfoByClass.get(modelClass)! const modelIdPropertyName = getModelIdPropertyName(modelClass) return { ...snapshot, [modelTypeKey]: modelInfo.name, [modelIdPropertyName]: internalId, } 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 }