Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion projects/natural/src/lib/classes/abstract-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export class NaturalAbstractDetail<Tone,

public static getFormGroup(model, service) {
const formConfig = service.getFormConfig(model);
return new FormGroup(formConfig, {validators: service.getFormGroupValidators()});
return new FormGroup(formConfig, {
validators: service.getFormGroupValidators(),
asyncValidators: service.getFormGroupAsyncValidators()
});
}

/**
Expand Down
42 changes: 42 additions & 0 deletions projects/natural/src/lib/classes/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {AbstractControl, AsyncValidatorFn, ValidationErrors} from '@angular/forms';
import { NaturalQueryVariablesManager } from './query-variable-manager';
import { NaturalAbstractModelService } from '../services/abstract-model.service';
import { map } from 'rxjs/operators';
import { Observable, of } from 'rxjs';

export class NaturalValidators {

/**
* Returns an async validator function that checks that the form control value is unique
*/
public static unique(
fieldName: string,
modelService: NaturalAbstractModelService<any, any, any, any, any, any, any, any, any>): AsyncValidatorFn {

const validator = (control: AbstractControl): Observable<ValidationErrors | null> => {

const condition = {};

if (control.value) {

condition[fieldName] = {equal: {value: control.value}};
const variables: any = {
pagination: {pageIndex: 0, pageSize: 0},
filter: {groups: [{conditions: [condition]}]},
};

const qvm = new NaturalQueryVariablesManager<any>();
qvm.set('variables', variables);

return modelService.count(qvm).pipe(
map((count: number) => {
return count > 0 ? {duplicateValue: count} : null;
}),
);
}

return of(null);
};
return validator;
}
}
17 changes: 17 additions & 0 deletions projects/natural/src/lib/services/abstract-model.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ export abstract class AbstractModelServiceSpec {
expect(Object.keys(object).length).toBeGreaterThan(keysAfterCreation); // should show created + updated objects merged
})),
);

it('should count existing values',
fakeAsync(inject([serviceClass], (service: ModelService) => {
const qvm = new NaturalQueryVariablesManager<any>();
const variables: any = {
pagination: {pageIndex: 0, pageSize: 0},
filter: {groups: [{conditions: [{slug: 'test string'}]}]},
};
qvm.set('variables', variables);
expect(() => service.count(qvm).subscribe()).toEqual(1);

variables['filter']['groups'][0]['conditions'][0]['slug'] = 'other string';
qvm.set('variables', variables);
expect(() => service.count(qvm).subscribe()).toEqual(0);

})),
);
}

private static expectNotConfiguredOrEqual(expectSuccess: boolean,
Expand Down
61 changes: 59 additions & 2 deletions projects/natural/src/lib/services/abstract-model.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidatorFn } from '@angular/forms';
import gql from 'graphql-tag';
import { Apollo } from 'apollo-angular';
import { NetworkStatus } from 'apollo-client';
import { FetchResult } from 'apollo-link';
Expand All @@ -10,11 +10,16 @@ import { NaturalFormControl } from '../classes/form-control';
import { NaturalQueryVariablesManager } from '../classes/query-variable-manager';
import { NaturalUtility } from '../classes/utility';
import { Literal } from '../types/types';
import {AsyncValidatorFn, ValidatorFn} from '@angular/forms';

export interface FormValidators {
[key: string]: ValidatorFn[];
}

export interface FormAsyncValidators {
[key: string]: AsyncValidatorFn[];
}

export interface VariablesWithInput {
input: Literal;
}
Expand Down Expand Up @@ -67,16 +72,31 @@ export abstract class NaturalAbstractModelService<Tone,
return {};
}

/**
* List of individual async fields validators
*/
public getFormAsyncValidators(): FormAsyncValidators {
return {};
}

/**
* List of grouped fields validators (like password + confirm password)
*/
public getFormGroupValidators(): ValidatorFn[] {
return [];
}

/**
* List of async group fields validators (like unique constraint on multiple columns)
*/
public getFormGroupAsyncValidators(): AsyncValidatorFn[] {
return [];
}

public getFormConfig(model: Literal): Literal {
const values = this.getConsolidatedForClient();
const validators = this.getFormValidators();
const asyncValidators = this.getFormAsyncValidators();
const config = {};
const disabled = model.permissions ? !model.permissions.update : false;

Expand All @@ -92,8 +112,9 @@ export abstract class NaturalAbstractModelService<Tone,
disabled: disabled,
};
const validator = typeof validators[key] !== 'undefined' ? validators[key] : null;
const asyncValidator = typeof asyncValidators[key] !== 'undefined' ? asyncValidators[key] : null;

config[key] = new NaturalFormControl(formState, validator);
config[key] = new NaturalFormControl(formState, validator, asyncValidator);
}

// Configure form for extra validators that are not on a specific field
Expand All @@ -108,6 +129,19 @@ export abstract class NaturalAbstractModelService<Tone,
}
}

for (const key of Object.keys(asyncValidators)) {
if (config[key] && asyncValidators[key]) {
config[key].setAsyncValidators(asyncValidators[key]);
} else {
const formState = {
value: model[key] ? model[key] : null,
disabled: disabled,
};

config[key] = new NaturalFormControl(formState, null, asyncValidators[key]);
Copy link
Member

@sambaptista sambaptista May 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about that "null". Don't change that for now, I'll test it by myself, and implement tests.

}
}

return config;
}

Expand Down Expand Up @@ -456,6 +490,29 @@ export abstract class NaturalAbstractModelService<Tone,
return input;
}

/**
* Return the number of objects matching the query
*
* This is used for the unique validator
*
* @TODO: use debounce or Observable.timer to avoid sending a query too often
*/
public count(queryVariablesManager: NaturalQueryVariablesManager<Vall>): Observable<number> {
const plural = NaturalUtility.makePlural(this.name);
let query = 'query Count' + NaturalUtility.upperCaseFirstLetter(plural);
query += '($filter: ' + NaturalUtility.upperCaseFirstLetter(this.name) + 'Filter) {';
query += plural + '(filter: $filter, pagination: {pageSize: 0}) { length } }';
query = gql(query);
return this.apollo.query({
query: query,
variables: queryVariablesManager.variables.value,
}).pipe(
map((result) => {
return result.data[plural].length as number;
}),
);
}

/**
* Return empty object with some default values from server perspective
*
Expand Down
1 change: 1 addition & 0 deletions projects/natural/src/lib/testing/mock-apollo.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const typeDefs = `

type Post {
id: ID!
slug: String
blog: Blog
}

Expand Down
1 change: 1 addition & 0 deletions projects/natural/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './lib/classes/data-source';
export * from './lib/classes/form-control';
export * from './lib/classes/query-variable-manager';
export * from './lib/classes/utility';
export * from './lib/classes/validators';

export * from './lib/services/abstract-model.service';
export * from './lib/services/enum.service';
Expand Down