import ViewModelCache from '../ViewModelCache'; import { FieldDataMappingRaw, ViewModelConstructor } from '../ViewModelFactory'; import Field, { FieldProps } from './Field'; export declare type RelatedViewModelValueType> = InstanceType; export declare type BaseRelatedViewModelValueType> = RelatedViewModelValueType | RelatedViewModelValueType[]; export declare type RelatedViewModelParsableType> = FieldDataMappingRaw | FieldDataMappingRaw[]; /** * @expandproperties */ export declare type RelatedViewModelFieldProps, FieldValueT, SourceFieldNameT extends string> = FieldProps & { /** * The name of the field on the [ViewModel](doc:viewModelFactory) that stores the * ID for this relation */ sourceFieldName: SourceFieldNameT; /** * Either a [ViewModel](doc:viewModelFactory), a function that returns a [ViewModel](doc:viewModelFactory) * or a function that returns a `Promise` that resolves to a [ViewModel](doc:viewModelFactory). */ to: (() => Promise | TargetViewModelT) | TargetViewModelT; /** * The cache to use to retrieve related records from. Uses the default model cache if not specified. */ cache?: ViewModelCache; }; /** * Thrown when a related view model is accessed but the related view model has not been resolved. */ export declare class UnresolvedRelatedViewModelFieldError, FieldValueT extends BaseRelatedViewModelValueType, ParsableValueT extends RelatedViewModelParsableType> extends Error { field: BaseRelatedViewModelField; constructor(field: BaseRelatedViewModelField, message: any); } /** * We split into RelatedViewModelField (for single records) and ManyRelatedViewModelField (for multiple records) * mainly to making typing easier. * * Use `ManyRelatedViewModelField` if `sourceFieldName` refers to a `ListField` otherwise `RelatedViewModelField`. */ export declare abstract class BaseRelatedViewModelField, FieldValueT extends BaseRelatedViewModelValueType, ParsableValueT extends RelatedViewModelParsableType, SourceFieldNameT extends string = string> extends Field { private _loadTo; private _resolvedTo; private _resolvingTo?; private _cache?; sourceFieldName: SourceFieldNameT; sourceField: Field; get many(): boolean; constructor(props: RelatedViewModelFieldProps); /** * @hidden */ contributeToClass(viewModel: TargetViewModelT): void; _isResolvingDeps: boolean; /** * Resolves the ViewModel this field links to. This is only necessary when `to` is a function that returns a * Promise (e.g. when using dynamic imports). This ensures the ViewModel is loaded. * * This needs to be called manually before `to` can be accessed. */ resolveViewModel(): Promise; /** * Compares to relations for equality - if the ViewModel has the same data this returns true */ isEqual(value1: FieldValueT, value2: FieldValueT): boolean; /** * Get the [ViewModel](doc:BaseViewModel) this related field is to. * * If `to` was defined as a function returning a `Promise` then you must call `resolveViewModel` * and wait for the returned `Promise` to resolve before accessing this otherwise an error will be thrown */ get to(): TargetViewModelT; get cache(): ViewModelCache; } /** * Define a field that references another ViewModel * * This requires two things: * * 1) The ViewModel to reference * 2) The field on the source ViewModel that contains the ID for the relation * * If you have multiple values use [ManyRelatedViewModelField](doc:ManyRelatedViewModelField) instead. * * ## Usage * * In the following example `User` has a `Group` as a relation. The id for the * connected group is stored on the `groupId` field: * * ```js * class Group extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) {} * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * groupId: new IntegerField(), * group: new RelatedViewModelField({ * to: Group, * sourceFieldName: 'groupId', * }), * }, { pkFieldName: 'id' }) {} * ``` * * You can add data to the cache, including nested relations: * * ```js * User.cache.add({ id: 1, name: 'Bob', group: { id: 1, name: 'Staff' } }); * ``` * * The `groupId` on the `User` will automatically set to `group.id`: * * ```js * User.cache.get(1, '*'); * // Output: { id: 1, name: 'Bob', groupId: 1, group: { id: 1, name: 'Staff' }} * ``` * * Related data is fetched from the related model cache automatically so is always in sync: * * ```js * // Change the group name * Group.cache.add({ id: 1, name: 'All Staff' }); * // It's reflected in the nested Group record returned by the User cache * User.cache.get(1, '*'); * // Output: { id: 1, name: 'Bob', groupId: 1, group: { id: 1, name: 'All Staff' }} * ``` * * ### Circular references * * The `to` field can also be a function to support circular references: * * ```js * class Group extends viewModelFactory({ * name: new CharField(), * ownerId: new IntegerField(), * owner: new RelatedViewModelField({ * to: () => User, * sourceFieldName: 'ownerId', * }), * }, { pkFieldName: 'id' }) {} * class User extends viewModelFactory({ * name: new CharField(), * groupId: new IntegerField(), * group: new RelatedViewModelField({ * to: Group, * sourceFieldName: 'groupId', * }), * }, { pkFieldName: 'id' }) {} * ``` * * > NOTE: If using typescript you will get some type errors with this approach. See the [circular references example](#example-circular-references-typing) * > for an approach to resolve this. * * You can query the circular relations as deep as you want: * * ```js * Group.cache.add({ id: 1, name: 'Staff', ownerId: 1 }); * User.cache.add({ id: 1, name: 'Bob', groupId: 1 }); * User.cache.get(1, ['name', 'group', ['group', 'owner'], ['group', 'owner', 'group']]); * // { * // groupId: 1, * // id: 1, * // name: "Bob", * // group: { * // id: 1, * // name: "Staff", * // ownerId: 1, * // owner: { * // groupId: 1, * // id: 1, * // name: "Bob", * // group: { * // id: 1, * // name: "Staff", * // ownerId: 1 * // } * // } * // } * // } * ``` * * ### Lazy loading * * `to` can also be a function that returns a Promise. This is useful to * lazy load modules: * * ```js * class Subscription extends viewModelFactory({ * id: new IntegerField(), * userId: new IntegerField(), * user: new RelatedViewModelField({ * sourceFieldName: 'userId', * to: async () => { * const User = await import('./User').default; * return User; * } * }) * }, { pkFieldName: 'id' }) {} * ``` * * **NOTE:** When you return a promise you have to call `resolveViewModel` on * that field before it's usable: * * ```js * await Subscription.fields.user.resolveViewModel() * ``` * * Failure to do this will result in an error being thrown the first time it's accessed. * * @extractdocs * @menugroup Fields * @typeName TargetViewModelT The [ViewModel](doc:BaseViewModel) class this field links to */ export declare class RelatedViewModelField, SourceFieldNameT extends string = string> extends BaseRelatedViewModelField, FieldDataMappingRaw, SourceFieldNameT> { static fieldClassName: string; /** * Converts a value into the relations [ViewModel](doc:viewModelFactory) instance. */ normalize(value: any): RelatedViewModelValueType; /** * Converts the linked record to a plain javascript object */ toJS(value: RelatedViewModelValueType): Record; } /** * Define a field that contains multiple records from another ViewModel * * This behaves the same as [RelatedViewModelField](doc:RelatedViewModelField) but `sourceFieldName` * must refer to a [ListField](doc:ListField) and all values are an array instead of a single value. * * ## Usage * * In the following example `User` has a many relation to `Group`. The ids for the * connected groups is stored on the `groupIds` field: * * ```js * class Group extends viewModelFactory( * { * id: new IntegerField(), * name: new CharField(), * }, * { pkFieldName: 'id' } * ) {} * * class User extends viewModelFactory( * { * id: new IntegerField(), * name: new CharField(), * groupIds: new ListField({ childField: new IntegerField() }), * groups: new ManyRelatedViewModelField({ * to: Group, * sourceFieldName: 'groupIds', * }), * }, * { pkFieldName: 'id' } * ) {} * ``` * * Data can be added to the cache using the `add` method: * * ```js * Group.cache.add([ * { id: 1, name: 'Admins' }, * { id: 2, name: 'Managers' }, * { id: 3, name: 'Customer Support' }, * { id: 4, name: 'Tech Support' }, * { id: 5, name: 'Sales' }, * ]); * User.cache.addList([ * { id: 1, name: 'Dave', groupIds: [1, 4] }, * { id: 2, name: 'Sarah', groupIds: [3, 4, 5] }, * { id: 3, name: 'Jen', groupIds: [1, 2, 3] }, * ]); * ``` * * Fetching nested records will automatically fetch related records from the corresponding cache: * * ```js * User.cache.get(1, ['name', 'groups']); * // Output: * // { * // groupIds: [ * // 1, * // 4 * // ], * // id: 1, * // name: Dave, * // groups: [ * // { * // id: 1, * // name: Admins * // }, * // { * // id: 4, * // name: Tech Support * // } * // ] * // } * ``` * * You can also add related records by nesting the data. This will automatically fill out the `groupIds` field and add * the related records to the `Group` cache: * * ```js * const user = User.cache.add({ * id: 4, * name: 'Bob', * groups: [{ id: 4, name: 'Tech Support', ownerId: 1 }], * }); * console.log(user.groupIds) * // Output: [4] * console.log(Group.cache.get(4, ['name', 'ownerId'])); * // Output: { id: 4, name: "Tech Support", ownerId: 1 } * ``` * * @extractdocs * @menugroup Fields * @typeName TargetViewModelT The [ViewModel](doc:BaseViewModel) class this field links to */ export declare class ManyRelatedViewModelField, SourceFieldNameT extends string = string> extends BaseRelatedViewModelField[], FieldDataMappingRaw[], SourceFieldNameT> { static fieldClassName: string; /** * Converts a value into the relations [ViewModel](doc:viewModelFactory) instance. */ normalize(value: any): RelatedViewModelValueType[]; /** * Converts the linked record to a plain javascript object */ toJS(value: RelatedViewModelValueType[]): Record; }