-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Prepare hyrest and hyrest-mobx for array types
- Loading branch information
Showing
21 changed files
with
473 additions
and
197 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ValidationStatus } from "./validation-status"; | ||
|
||
export interface BaseField<TModel> { | ||
/** | ||
* The validation status for the current value. | ||
* Always held in synchronization with the value. | ||
*/ | ||
status: ValidationStatus; | ||
|
||
/** | ||
* The actual, unwrapped value for this field. | ||
* When called on a structure, this will create a real model. | ||
*/ | ||
value: TModel; | ||
|
||
/** | ||
* Whether the current value is valid. | ||
* Syntactic sugar for `status === ValidationStatus.VALID`. | ||
*/ | ||
valid: boolean; | ||
|
||
/** | ||
* Whether the current value is invalid. | ||
* Syntactic sugar for `status === ValidationStatus.INVALID`. | ||
*/ | ||
invalid: boolean; | ||
|
||
/** | ||
* Whether the validation for the current value is still in progress. | ||
* Syntactic sugar for `status === ValidationStatus.IN_PROGRESS`. | ||
*/ | ||
inProgress: boolean; | ||
|
||
/** | ||
* Whether the value has never been set before. | ||
* Syntactic sugar for `status === ValidationStatus.UNTOUCHED`. | ||
*/ | ||
untouched: boolean; | ||
|
||
/** | ||
* Can be called to update the value, for example when the user typed | ||
* something in a related field. | ||
* | ||
* When called on a structure, this will update all underlying fields | ||
* recursively. | ||
*/ | ||
update(newValue: TModel): Promise<void>; | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { bind } from "bind-decorator"; | ||
import { | ||
PropertyMeta, | ||
getPropertyValidation, | ||
Constructable, | ||
populate, | ||
ValidationOptions, | ||
universal, | ||
} from "hyrest"; | ||
import { observable, computed, action } from "mobx"; | ||
import { ValidationStatus } from "./validation-status"; | ||
import { ContextFactory } from "./context-factory"; | ||
import { BaseField } from "./base-field"; | ||
|
||
export class FieldArray<TModel, TContext> extends Array<TModel> implements ReadonlyArray<TModel>, BaseField<TModel[]> { | ||
/** | ||
* The validation status for this field. | ||
*/ | ||
@observable public status = ValidationStatus.UNTOUCHED; | ||
|
||
/** | ||
* A validator if the developer used a `@is` decorator on the class. | ||
*/ | ||
private validation?: ValidationOptions<TModel, TContext>; | ||
|
||
/** | ||
* The context factory provided by the `@hasFields` decorator, creating a context handed | ||
* down to the validator for the `.validateCtx` method. | ||
*/ | ||
private contextFactory: ContextFactory<TContext>; | ||
|
||
/** | ||
* The runtime-accessible type of the model. Specified using `@specify` or using reflection. | ||
*/ | ||
private modelType: Constructable<TModel>; | ||
|
||
constructor ( | ||
modelType: Constructable<TModel>, | ||
contextFactory: ContextFactory<TContext>, | ||
validation: ValidationOptions<TModel, TContext>, | ||
) { | ||
super(); | ||
this.contextFactory = contextFactory; | ||
this.validation = validation; | ||
this.modelType = modelType; | ||
} | ||
|
||
// public get array(): FieldArray<TModel[0], TContext> { | ||
// if (this._array) { return this._array; } | ||
// if (!Array.isArray(this.model) && this.model !== null && typeof this.model !== "undefined") { | ||
// throw new Error("Can't create a field array from a non-Array type."); | ||
// } | ||
// this._array = new FieldArray(this.model as any as any[]); | ||
// } | ||
public get value(): TModel[] { | ||
throw new Error("TODO"); | ||
} | ||
|
||
@computed public get valid(): boolean { return this.status === ValidationStatus.VALID; } | ||
@computed public get invalid(): boolean { return this.status === ValidationStatus.INVALID; } | ||
@computed public get inProgress(): boolean { return this.status === ValidationStatus.IN_PROGRESS; } | ||
@computed public get untouched(): boolean { return this.status === ValidationStatus.UNTOUCHED; } | ||
|
||
@bind @action public async update(newValue: TModel[]) { | ||
throw new Error("TODO"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { bind } from "bind-decorator"; | ||
import * as invariant from "invariant"; | ||
import { | ||
PropertyMeta, | ||
getPropertyValidation, | ||
Constructable, | ||
populate, | ||
ValidationOptions, | ||
universal, | ||
} from "hyrest"; | ||
import { observable, computed, action } from "mobx"; | ||
import { ValidationStatus } from "./validation-status"; | ||
import { ContextFactory } from "./context-factory"; | ||
import { BaseField } from "./base-field"; | ||
import { Fields } from "./fields"; | ||
import { createField } from "./field"; | ||
import { FieldArray } from "./field-array"; | ||
|
||
export class FieldSimple<TModel, TContext = any> implements BaseField<TModel> { | ||
/** | ||
* The validation status for this field. | ||
*/ | ||
@observable public status = ValidationStatus.UNTOUCHED; | ||
|
||
/** | ||
* Nested fields of this field. | ||
*/ | ||
public nested: Fields<TModel, TContext>; | ||
|
||
/** | ||
* The `nested` property on this class is by default initialized using `undefined` for | ||
* every property to allow for lazy initialization of deeply nested structures. | ||
* This is an internal map for keeping the real values. | ||
*/ | ||
private _nested: Fields<TModel, TContext>; | ||
|
||
/** | ||
* The runtime-accessible type of the model. Specified using `@specify` or using reflection. | ||
*/ | ||
private modelType: Constructable<TModel>; | ||
|
||
/** | ||
* A validator if the developer used a `@is` decorator on the class. | ||
*/ | ||
private validation?: ValidationOptions<TModel, TContext>; | ||
|
||
/** | ||
* The actual value for this field if it doesn't have sub-fields. | ||
*/ | ||
@observable private model: TModel = undefined; | ||
|
||
/** | ||
* Property metadata gathered from the `universal` scope about the `modelType`. | ||
* Cached and initialized from the constructor. | ||
*/ | ||
private properties: PropertyMeta[]; | ||
|
||
/** | ||
* The context factory provided by the `@hasFields` decorator, creating a context handed | ||
* down to the validator for the `.validateCtx` method. | ||
*/ | ||
private contextFactory: ContextFactory<TContext>; | ||
|
||
constructor( | ||
modelType: Constructable<TModel>, | ||
contextFactory: ContextFactory<TContext>, | ||
validation?: ValidationOptions<TModel, TContext>, | ||
) { | ||
// Copy all arguments that should be stored. | ||
this.contextFactory = contextFactory; | ||
this.validation = validation; | ||
this.modelType = modelType; | ||
// Gather and cache all property metadata from the universal scope. | ||
this.properties = universal.propertiesForClass(this.modelType); | ||
// Initialize the map of real, underlying values for each nested field to contain an `undefined` | ||
// value for each key. This is necessary for Mobx, as the addition of new keys later on will not trigger | ||
// the observable. | ||
this._nested = this.properties.reduce((result, { target, property, expectedType }) => { | ||
// Need to initialize the whole fields map with `undefined` to help frameworks such as MobX. | ||
result[property as keyof TModel] = undefined; | ||
return result; | ||
}, {} as Fields<TModel, TContext>); | ||
// Create the outfacing `nested` property: Create a getter for each property which lazily initializes | ||
// and caches the real value in `_nested`. | ||
this.nested = this.properties.reduce((result, propertyMeta) => { | ||
// Create a getter on the `fields` property which will lazily initialize all `Field`s. | ||
Object.defineProperty(result, propertyMeta.property, { | ||
get: () => { | ||
const key = propertyMeta.property as keyof TModel; | ||
if (this._nested[key] !== undefined) { return this._nested[key]; } | ||
// The cast to `any` are neccessary as Typescript cannot deal with this | ||
// kind of types. See https://github.com/Microsoft/TypeScript/issues/22628 (for example). | ||
this._nested[key] = | ||
createField<TModel[keyof TModel], TContext>(propertyMeta, this.contextFactory) as any; | ||
}, | ||
enumerable: true, | ||
}); | ||
return result; | ||
}, {} as Fields<TModel, TContext>); | ||
} | ||
|
||
/** | ||
* Determines whether this is a managed property. | ||
* If the developer defined any validation decorators (`@is`) on this class, it is managed and therefore | ||
* for example a new `Field` instance needs to be created. Also, no value will be managed in this instance. | ||
*/ | ||
private get isManaged() { | ||
return this.properties.length !== 0; | ||
} | ||
|
||
public get value(): TModel { | ||
if (this.isManaged) { | ||
const obj = Object.keys(this._nested).reduce((result: TModel, key) => { | ||
const modelKey = key as keyof TModel; | ||
const field = this._nested[modelKey]; | ||
if (!field) { return result; } | ||
invariant( | ||
field instanceof FieldSimple || field instanceof FieldArray, | ||
"Found an invalid wrapped field value.", | ||
); | ||
if (field instanceof FieldSimple) { | ||
result[modelKey] = field.value; | ||
} else { | ||
result[modelKey] = field.value as TModel[keyof TModel]; | ||
} | ||
return result; | ||
}, {}); | ||
return populate(this.modelType, obj); | ||
} | ||
return this.model; | ||
} | ||
|
||
@computed public get valid(): boolean { return this.status === ValidationStatus.VALID; } | ||
@computed public get invalid(): boolean { return this.status === ValidationStatus.INVALID; } | ||
@computed public get inProgress(): boolean { return this.status === ValidationStatus.IN_PROGRESS; } | ||
@computed public get untouched(): boolean { return this.status === ValidationStatus.UNTOUCHED; } | ||
|
||
@bind @action public async update(newValue: TModel) { | ||
if (this.isManaged) { | ||
await Promise.all(Object.keys(newValue).map((key) => { | ||
const modelKey = key as keyof TModel; | ||
const field = this._nested[modelKey]; | ||
invariant( | ||
field instanceof FieldSimple || field instanceof FieldArray, | ||
"Found an invalid wrapped field value.", | ||
); | ||
if (field instanceof FieldSimple) { | ||
return field.update(newValue[modelKey]); | ||
} | ||
if (field instanceof FieldArray) { | ||
return field.update(newValue[modelKey] as TModel[keyof TModel] & any[]); | ||
} | ||
})); | ||
return; | ||
} | ||
this.model = newValue; | ||
if (this.validation.fullValidator) { | ||
this.status = ValidationStatus.IN_PROGRESS; | ||
const processed = await this.validation.fullValidator( | ||
newValue, | ||
{ context: this.contextFactory() }, | ||
); | ||
this.status = processed.hasErrors ? ValidationStatus.INVALID : ValidationStatus.VALID; | ||
} | ||
} | ||
} |
Oops, something went wrong.