import Field, { RecordBoundField } from './fields/Field'; import { BaseRelatedViewModelField } from './fields/RelatedViewModelField'; import { ViewModelFieldPaths } from './fieldUtils'; import ViewModelCache from './ViewModelCache'; export declare type SinglePrimaryKey = string | number; export declare type CompoundPrimaryKey = { [fieldName: string]: SinglePrimaryKey; }; export declare type PrimaryKey = SinglePrimaryKey | CompoundPrimaryKey; export declare type FieldsMapping = { [fieldName: string]: Field; }; export declare type FieldsMappingOrNull = Record> | { [K in keyof T]?: null | undefined | Field; }; declare type ExtractRelatedFields> = { [P in keyof T['fields'] as T['fields'][P] extends BaseRelatedViewModelField ? P : never]: T['fields'][P] extends BaseRelatedViewModelField ? X : never; }; declare type ValueOf = T[keyof T]; declare type FieldPathInner, R extends ExtractRelatedFields = ExtractRelatedFields> = [...FieldNames, keyof T['fields']] | [ ...FieldNames, ...ValueOf<{ [K in Extract]: ValueOf<{ [J in Extract]: [K, J] | (R[K]['fields'][J] extends BaseRelatedViewModelField ? FieldPathInner<[K, J], X> : never); }>; }> ]; /** * @typename string|string[] */ export declare type FieldPath, R extends ExtractRelatedFields = ExtractRelatedFields> = Extract | ValueOf<{ [K in Extract]: ValueOf<{ [J in Extract]: [K, J] | (R[K]['fields'][J] extends BaseRelatedViewModelField ? FieldPathInner<[K, J], X> : never); }>; }>; /** * @typename '*'|[string|string[]][] */ export declare type FieldPaths, R extends ExtractRelatedFields = ExtractRelatedFields> = '*' | FieldPath[]; declare type _ExtractRootFieldNames, FieldNames extends FieldPath> = Extract ? T['fields'][FieldNames[0]] extends BaseRelatedViewModelField ? SourceFieldNameT extends keyof T['fields'] ? FieldNames[0] | SourceFieldNameT : FieldNames[0] : FieldNames[0] : FieldNames, string>; /** * Given a FieldPath return the root field name of each path, eg. * ['id', 'name', ['group', 'owner']] * would return ['id', 'name', 'group'] */ declare type ExtractRootFieldNames, FieldNames extends FieldPath> = _ExtractRootFieldNames extends keyof T['fields'] ? _ExtractRootFieldNames : never; export declare type FieldDataMapping = { readonly [K in keyof FieldMappingType]: FieldMappingType[K]['__fieldValueType']; }; export declare type FieldDataMappingRaw = { [K in keyof T]?: T[K]['__parsableValueType']; }; /** * @typename string */ export declare type ExtractFieldNames = Extract; /** * Extract fields when specified using '*'. Currently this is all fields but could change to be * non-relation fields only */ export declare type ExtractStarFieldNames = ExtractFieldNames; declare type ExtractPkFieldTypes = ExtractFieldNames | ExtractFieldNames[]; declare type ViewModelPkFieldType = Extract | Extract[]; /** * Flatten a nested path to a single level with dot notation * * eg. * ``` * flattenFieldPath([ * 'id', * ['user', ['group', 'id']], * ['user', 'groupId'], * ['user', 'id'], * 'userId' * ]) * // ['id', 'user.group.id', 'user.groupId', 'user.id', 'userId'] * ``` */ export declare function flattenFieldPath, R extends ExtractRelatedFields = ExtractRelatedFields>(fieldPath: FieldPath[] | FieldPath, separator?: string): string[]; /** * @expandproperties */ export interface ViewModelOptions | ExtractFieldNames[]> { /** * Optional base class to extend. This must extend [BaseViewModel](doc:BaseViewModel). * * When calling `augment` this is set the augmented class. * * @typename Class */ baseClass?: ViewModelConstructor; /** * Primary key name(s) to use. There should be field(s) with the corresponding name in the * provided `fields`. * * Only `pkFieldName` or `getImplicitPkField` should be provided. If neither are provided then * a field called `id` will be used and created if not provided in `fields`. * * @typename string|string[] */ pkFieldName: PkFieldNameT; } declare type KeysOfType = keyof { [P in keyof O as O[P] extends T ? P : never]: O[P]; }; declare type AugmentFields> = Omit & Omit>; declare type ExtractPkFields> = PkFieldType extends string ? Record : { [K in keyof FieldMappingType as K extends PkFieldType[number] ? K : never]: FieldMappingType[K]; }; declare type ViewModelInterfaceInputData> = FieldDataMappingRaw & { [K in keyof ExtractPkFields]: ExtractPkFields[K]['__parsableValueType']; }; /** * Thrown when cloning a record and requested fields cannot be found * * ```js * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) { * } * const user = new User({ id: 1 }); * try { * const user2 = user.clone(['id', 'name']) * } catch (e) { * console.log(e.missingFieldNames); * // ['name'] * console.log(e.message) * // Can't clone User with fields id, name as only these fields are set: id. * // Missing fields: name * } * ``` * * @extractdocs * @menugroup Errors */ export declare class MissingFieldsError extends Error { /** * Any field names that were missing from the record */ missingFieldNames: string[]; /** * An array of 2-tuples: the first element is the relation field name and the second is an * array of field names missing from that record */ missingRelations: [string, string[]][]; /** * The field names that were set on the record */ assignedFields: string[]; constructor(record: ViewModelInterface, assignedFields: string[], requestedFieldNames: FieldPath[], missingFieldNames: string[], missingRelations: [string, string[]][]); } /** * The base class all ViewModel classes will extend. See [viewModelFactory](doc:viewModelFactory) for how ViewModel * classes are created. * * If you use the [baseClass](doc:viewModelFactory##method-viewmodel) option the class passed must extend * `BaseViewModel`. * * ## Types * * ### PartialViewModel * * This is a type used to represent a ViewModel with some or fields set. It still refers to an instance of `BaseViewModel`, * but also specifies as part of the type which fields are set. If you see this in the documentation know that it is just * an instance of a `ViewModel`. * * You can use it in your own types. For example, this is a function that returns a list of User records, but allows you * to specify which fields to return: * * ```typescript * function getList>( * cache: ViewModelCache, * result?: T[] | null, * fieldNames?: (keyof typeof User['fields'])[] * ): T[] => { * if (!result) { * return []; * } * if (!fieldNames) { * return cache.getList(result, true); * } * return cache.getList( * result.map(r => r._key), * fieldNames, * true * ); * } * ``` * * ### ViewModelConstructor * * This is a type used to represent the class of a ViewModel itself. Where this is used it's expected you pass * it the ViewModel class itself rather than an instance. * * @extractdocs */ export declare class BaseViewModel, AssignedFieldNames extends ExtractFieldNames = ExtractFieldNames> { /** * Get the actual ViewModel class for this instance */ get _model(): ViewModelConstructor; /** * Return the data for this record as a plain object * * @returntypename Record */ toJS(): { readonly [K in AssignedFieldNames]: FieldMappingType[K]['__fieldValueType']; }; /** * Compares two records to see if they are equivalent. * * - If the ViewModel is different then the records are always considered different * - If the records were initialised with a different set of fields then they are * considered different even if the common fields are the same and other fields are * all null * * @param record The record to compare to */ isEqual(record: ViewModelInterface | null): boolean; /** * Clone this record, optionally with only a subset of the fields * * Will throw [MissingFieldsError](doc:MissingFieldsError) if any of the requested * field names are not set on the record. * * @param fieldNames The names of fields to clone. Can be specified as an array of flat field names, '*' * for all fields or nested notation when dealing with relations eg. `[['relationName', 'relationFieldName']]` * @typeParam CloneFieldNames The names of the fields that are to be cloned. */ clone>(fieldNames?: CloneFieldNames[] | FieldPath>[]): ViewModelInterface; private __recordBoundFields; /** * Get fields bound to this record instance. Each field behaves the same as accessing it via ViewModel.fields but * has a `value` property that contains the value for that field on this record. * * This is useful when you need to know both the field on the ViewModel and the value on a record (eg. when formatting * a value from a record * * ```js * const user = new User({ name: 'Jon Snow' }); * user.name * // Jon Snow * user._f.name * // CharField({ name: 'name', label: 'Label' }); * user._f.name.value * // Jon Snow * ``` * * @typename {[fieldName: string]: Field} */ get _f(): { readonly [K in AssignedFieldNames]: RecordBoundField; }; /** * Instantiate the ViewModel with the specified data. * * Data for `RelatedViewModelField` and `ManyRelatedViewModelField` fields will be transformed into the corresponding * ViewModel instances, and the `sourceFieldName` for each will be set to the corresponding id(s) * * @param data The data to assign to the ViewModel. The accepted keys here match the fields the ViewModel was * created with. * * @paramtypename data Record */ constructor(data: { [K in AssignedFieldNames]: FieldMappingType[K]['__parsableValueType']; }); /** * Given `records` return the paths that are common between them. * * A naive solution is to just check `_assignedFieldsDeep`: * * ```js * const paths = intersectionBy( * ...assignedData[key].map(record => record._assignedFieldsDeep), * p => flattenFieldPath(p).join('|') * ); * ``` * * but that would fail if some records had a null value for a relation and others didn't. * * This function handles nested records such that a null relation is ignored. For example if you received: * * ```js * [ * { * id: 1, * nestedRecordId: null, * nestedRecord: null, * }, * { * id: 2, * nestedRecordId: 1, * nestedRecord: { * id: 1, * name: 'Nested Record 1', * }, * }, * { * id: 3, * nestedRecordId: 2, * nestedRecord: { * id: 2, * name: 'Nested Record 2', * otherField: 'Name', * }, * } * ] * ``` * * would result in * * ```js * ['id', 'nestedRecordId', ['nestedRecord', 'id'], ['nestedRecord', 'name']] * ``` * * Noting that the first record has no nested fields (because they are null) and so get's ignored, and the * last record has 'otherField' which the second doesn't so is excluded. * * @param records */ fieldPathIntersection(records: ViewModelInterface[]): FieldPath>[]; /** * The assigned data for this record. You usually don't need to access this directly; values * for a field can be retrieved from the record directly using the field name * * @typename Object */ readonly _data: { [k in AssignedFieldNames]: FieldMappingType[k]['__fieldValueType']; }; /** * List of field names with data available on this instance. * * @typename string[] */ readonly _assignedFields: string[]; /** * Deep field names set on this record. If no relations are set this is the same as `_assignedFields`. * * A deep field is a field that is a relation to another model and is represented as an array, eg. * `['group', 'name']` would be the the `name` field on the `group` relation. * * @typename string[] */ readonly _assignedFieldsDeep: FieldPath>[]; /** * The `ViewModelFieldPaths` instance for this record. This is a unique instance based on the actual * assigned fields and can be compared to other instances to determine if the same fields are set. */ readonly _assignedFieldPaths: ViewModelFieldPaths>; /** * Returns the primary key value(s) for this instance. This is to conform to the * [Identifiable](doc:Identifiable) interface. * * @typename PkFieldType */ get _key(): PkFieldType extends string ? FieldMappingType[PkFieldType]['__fieldValueType'] : { [K in keyof FieldMappingType as K extends PkFieldType[number] ? K : never]: FieldMappingType[K]['__fieldValueType']; }; } export declare type ViewModelInterface, AssignedFieldNames extends ExtractFieldNames = ExtractFieldNames> = BaseViewModel & FieldDataMapping>; /** * Check if an object is ViewModel * * ```js * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) { * * } * * isViewModelInstance(new User({ id: 1 })); * // true * ``` * * If you need to check if a class (rather than an instance) is a ViewModel use [isViewModelClass](doc:isViewModelClass). * * * @param object The object to check * @extractdocs */ export declare function isViewModelInstance>(object: any): object is T; /** * Check if a class is a ViewModel * * ```js * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) { * * } * * isViewModelClass(User); * // true * ``` * * * If you need to check if an instance (rather than a class) is a ViewModel use [isViewModelInstance](doc:isViewModelInstance). * * * @param cls The class to check * * @extractdocs */ export declare function isViewModelClass>(cls: any): cls is T; /** * @typename ViewModel Class * @typeParam FieldMappingType The fields for the ViewModel as an object mapping field names to field instances * * @extractdocs */ export interface ViewModelConstructor> { new >(data: D): ViewModelInterface>; new (data: ViewModelInterfaceInputData): ViewModelInterface>>; /** * The bound fields for this ViewModel. These will match the `fields` passed in to `ViewModel` with the * following differences: * - If a primary key is created for you this will exist here * - All fields are bound to the created class. This means you can access the `ViewModel` class from the field on * the `model` property, eg. `User.fields.email.model === User` will be true. * - All fields have the `name` property set to match the key in `fields` * - All fields have `label` filled out if not explicitly set (eg. if name was `emailAddress` label will be created * as `Email Address`) * * See also [getField](doc:viewModelFactory#method-getField) for getting a nested field using array notation. */ readonly fields: FieldMappingType; /** * The singular label for this ViewModel. This should be set by extending the created class. * * ```js * class User extends viewModelFactory(fields) { * static label = 'User'; * } * ``` */ readonly label: string; /** * The label used to describe an indeterminate number of this ViewModel. This should be set by extending the created class. * * ```js * class User extends viewModelFactory(fields) { * static labelPlural = 'Users'; * } * ``` */ readonly labelPlural: string; /** * Name of the primary key field for this ViewModel (or fields for compound keys) * * If `options.pkFieldName` is not specified a field will be created from `options.getImplicitPk` * if provided otherwise a default field with name 'id' will be created. * * @typename string */ readonly pkFieldName: PkFieldType; /** * Shortcut to get pkFieldName as an array always, even for non-compound keys * * @typename string[] */ readonly pkFieldNames: PkFieldType extends string ? [PkFieldType] : PkFieldType; /** * Shortcut to get the names of all fields excluding primary keys. * * If you want all fields including primary key use `allFieldNames` * * @typename string[] */ readonly fieldNames: ExtractFieldNames[]; /** * Shortcut to get all field names including primary keys * * @typename string[] */ readonly allFieldNames: ExtractFieldNames[]; /** * Shortcut to get the names of all relation fields * * @typename string[] */ readonly relationFieldNames: Extract>, string>[]; /** * Get a field from this model or a related model * * Accepts either a string for a field on this record or array notation for traversing [RelatedViewModelField](doc:RelatedViewModelField) * fields: * * ```jsx * Subscription.getField(['user', 'group', 'owner']) * ``` * @param fieldName Either a string or an array of strings where the last element is the final field name to return * and each other element is a [RelatedViewModelField](doc:RelatedViewModelField) on a ViewModel. */ getField(fieldName: FieldPath, ExtractRelatedFields>>): Field; /** * The cache instance for this ViewModel. A default instance of [ViewModelCache](doc:ViewModelCache) * is created when first accessed or you can explicitly assign a cache: * * ```js * class User extends viewModelFactory(fields) { * static cache = new MyCustomCache(User); * } * ``` */ cache: ViewModelCache; /** * Create a new class that extends this class with the additional specified fields. To remove a * field that exists on the base class set it's value to null. * * ```js * class Base extends viewModelFactory({ * id: new NumberField({ * label: 'Id', * }), * firstName: new CharField({ * label: 'First Name', * }), * lastName: new CharField({ * label: 'Last Name', * }), * email: new EmailField({ * label: 'Email', * }), * }, {pkFieldName: 'id'}) { * static label = 'User'; * static labelPlural = 'Users'; * } * * class User extends BaseUser.augment({ * region: new IntegerField({ * label: 'region', * required: true, * helpText: 'Region Coding of the user', * choices: [ * [1, 'Oceania'], * [2, 'Asia'], * [3, 'Africa'], * [4, 'America'], * [5, 'Europe'], * [6, 'Antarctica'], * [7, 'Atlantis'], * ], * }), * photo: new ImageField({ * helpText: 'Will be cropped to 400x400', * }), * }) { * } * * // true * User instanceof BaseUser * // true * User.label === 'User' * * // ['firstName, 'lastName', 'email', 'region', 'photo] * User.fieldNames * ``` * * @param newFields Map of field name to a `Field` instance (to add the field) or `null` (to remove the field) * @param newOptions Provide optional overrides for the options that the original class was created with * @return A new ViewModel class with fields modified according to `newFields`. */ augment, AugmentPkFieldType extends ViewModelPkFieldType> = PkFieldType extends string ? T[PkFieldType] extends Field ? PkFieldType : any : any>(newFields: T, newOptions?: Partial, AugmentPkFieldType>>): ViewModelConstructor, AugmentPkFieldType>; } /** * Thrown when attempting to access a field that does not exist on a ViewModel * * @extractdocs * @menugroup Errors */ export declare class InvalidFieldError extends Error { } /** * Factory for creating ViewModel classes from the specified fields and options. * * > See the [ViewModels guide](/docs/getting-started/viewmodel) getting started guide * * ## Usage overview * * ```js * const fields = { * userId: new IntegerField({ label: 'User ID' }), * firstName: new CharField({ label: 'First Name' }), * // label is optional; will be generated as 'Last name' * lastName: new CharField(), * }; * // You must supplier 'pkFieldName' - everything else are optional * const options = { * pkFieldName: 'userId', * // Multiple names can be specified for compound keys * pkFieldName: ['organisationId', 'departmentId'], * // Optionally can specify a baseClass for this model. When using `augment` * // this is automatically set to the class being augmented. * baseClass: BaseViewModel, * }; * class User extends viewModelFactory(fields, options) { * // Optional; default cache is usually sufficient * static cache = new MyCustomCache(); * * // Used to describe a single user * static label = 'User'; * // User to describe an indeterminate number of users * static labelPlural = 'Users'; * } * ``` * * @param fields A map of field name to an instance of `Field` * @extractdocs * @returns A class that extends [BaseViewModel](doc:BaseViewModel) * @returntypename ViewModelClass * @typeParam FieldMappingType The fields for the ViewModel * @typeParam PkFieldNameT The primary key field name(s) */ export default function viewModelFactory | ExtractFieldNames[]>(fields: FieldMappingType, options: ViewModelOptions): ViewModelConstructor; /** * Type to describe values of an instance of ViewModel * * Usage: * * ```ts * const Person = viewModelFactory({ * name: new Field(), * age: new Field(), * }); * type PersonValues = ViewModelValues; * ``` */ export declare type ViewModelValues, FieldNames extends keyof T['fields'] = keyof T['fields'], OptionalFieldNames extends keyof T['fields'] = keyof T['fields']> = { [K in Extract]: T['fields'][K]['__fieldValueType']; } & { [K in Extract]?: T['fields'][K]['__fieldValueType']; }; /** * Type to describe a ViewModel instance with only some fields set * * Usage: * * ```ts * const Person = viewModelFactory({ * name: new Field(), * age: new Field(), * }); * type AgeOnly = PartialViewModel; * ``` */ export declare type PartialViewModel, FieldNames extends FieldPath = ExtractFieldNames> = Omit, ExtractFieldNames> & ViewModelInterface | (T['pkFieldName'] extends string ? T['pkFieldName'] : T['pkFieldName'][number])>; /** * Extracts the parseable type for primary key on a ViewModel * * @typename PkType */ export declare type ExtractPkFieldParseableValueType> = T['pkFieldName'] extends string ? T['fields'][T['pkFieldName']]['__parsableValueType'] : { [K in T['pkFieldNames']]: T['fields'][K]['__parsableValueType']; }; export {};