import type { O } from "ts-toolbelt" import type { AnyDataModel } from "../../dataModel/BaseDataModel" import { getDataModelMetadata } from "../../dataModel/getDataModelMetadata" import { isDataModelClass } from "../../dataModel/utils" import type { ModelClass, ModelData, ModelUntransformedData, } from "../../modelShared/BaseModelShared" import { modelInfoByClass } from "../../modelShared/modelInfo" import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo" import { noDefaultValue } from "../../modelShared/prop" import { failure, lazy } from "../../utils" import { getTypeInfo } from "../getTypeInfo" import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker" import type { AnyStandardType, IdentityType } from "../schemas" import { lateTypeChecker, TypeChecker, TypeCheckerBaseType, TypeInfo, type TypeInfoGen, } from "../TypeChecker" const cachedDataModelTypeChecker = new WeakMap, TypeChecker>() const dataModelDataStandardTypes = new WeakSet() type _Class = abstract new (...args: any[]) => T type _ClassOrObject = K extends M ? object : _Class | (() => _Class) /** * A type that represents a data model data. * The type referenced in the model decorator will be used for type checking. * * Example: * ```ts * const someDataModelDataType = types.dataModelData(SomeModel) * // or for recursive models * const someDataModelDataType = types.dataModelData(() => SomeModel) * ``` * * @template M Data model type. * @param modelClass Model class. * @returns */ export function typesDataModelData( modelClass: _ClassOrObject ): IdentityType< | ModelUntransformedData< K extends M ? (M extends AnyDataModel ? M : never) : K extends AnyDataModel ? K : never > | ModelData< K extends M ? (M extends AnyDataModel ? M : never) : K extends AnyDataModel ? K : never > > { // if we type it any stronger then recursive defs and so on stop working if (!isDataModelClass(modelClass) && typeof modelClass === "function") { // resolve later const modelClassFn = modelClass as () => ModelClass const typeInfoGen: TypeInfoGen = (t) => new DataModelDataTypeInfo(t, modelClassFn()) const tc = lateTypeChecker(() => typesDataModelData(modelClassFn()) as any, typeInfoGen) as any dataModelDataStandardTypes.add(tc as object) return tc } else { const modelClazz: ModelClass = modelClass as any const cachedTypeChecker = cachedDataModelTypeChecker.get(modelClazz) if (cachedTypeChecker) { return cachedTypeChecker as any } const typeInfoGen: TypeInfoGen = (t) => new DataModelDataTypeInfo(t, modelClazz) const tc = lateTypeChecker(() => { const modelInfo = modelInfoByClass.get(modelClazz)! const typeName = `DataModelData(${modelInfo.name})` const dataTypeChecker = getDataModelMetadata(modelClazz).dataType if (!dataTypeChecker) { throw failure( `type checking cannot be performed over data model data of type '${modelInfo.name}' since that model type has no data type declared, consider adding a data type or using types.unchecked() instead` ) } const resolvedDataTypeChecker = resolveTypeChecker(dataTypeChecker) const thisTc: TypeChecker = new TypeChecker( TypeCheckerBaseType.Object, (value, path, typeCheckedValue) => { return resolvedDataTypeChecker.check(value, path, typeCheckedValue) }, () => typeName, typeInfoGen, (value) => { return resolvedDataTypeChecker.snapshotType(value) ? thisTc : null }, (sn: Record) => { return resolvedDataTypeChecker.fromSnapshotProcessor(sn) }, (sn: Record) => { return resolvedDataTypeChecker.toSnapshotProcessor(sn) } ) return thisTc }, typeInfoGen) as any cachedDataModelTypeChecker.set(modelClazz, tc) dataModelDataStandardTypes.add(tc as object) return tc } } /** * `types.dataModelData` type info for a model props. */ export interface DataModelDataTypeInfoProps { readonly [propName: string]: Readonly<{ type: AnyStandardType | undefined typeInfo: TypeInfo | undefined hasDefault: boolean default: any }> } /** * `types.dataModelData` type info. */ export class DataModelDataTypeInfo extends TypeInfo { readonly kind = "dataModelData" readonly modelClass: ModelClass private _props = lazy(() => { const objSchema = getInternalModelClassPropsInfo(this.modelClass) const propTypes: O.Writable = {} Object.keys(objSchema).forEach((propName) => { const propData = objSchema[propName] const type = propData._typeChecker let typeInfo: TypeInfo | undefined if (type) { typeInfo = getTypeInfo(type) } let hasDefault = false let defaultValue: any if (propData._defaultFn !== noDefaultValue) { defaultValue = propData._defaultFn hasDefault = true } else if (propData._defaultValue !== noDefaultValue) { defaultValue = propData._defaultValue hasDefault = true } propTypes[propName] = { type, typeInfo, hasDefault, default: defaultValue, } }) return propTypes }) get props(): DataModelDataTypeInfoProps { return this._props() } get modelType(): string { const modelInfo = modelInfoByClass.get(this.modelClass)! return modelInfo.name } constructor(thisType: AnyStandardType, modelClass: ModelClass) { super(thisType) this.modelClass = modelClass } } /** * @internal */ export function registerDataModelDataStandardTypeResolver() { registerStandardTypeResolver((v) => (isDataModelClass(v) ? typesDataModelData(v) : undefined)) } /** * @internal */ export function isDataModelDataStandardType(value: unknown): boolean { return typeof value === "object" || typeof value === "function" ? dataModelDataStandardTypes.has(value as object) : false }