How to validate child DTO conditionally with class-validator? - class-validator

I am trying to conditionally validate nested DTO with class validator, but #ValidateIf seems to be not applying correctly and always validating the nested class.
I have the following DTO:
export class SuperAdminStoreDto extends StoreDto {
#IsOptional()
#IsBoolean()
readonly payments: boolean;
#ValidateIf(object => object.payments)
#ValidateNested({ each: true })
#Type(() => PaymentDetails)
readonly payment_details: PaymentDetails[];
}
class PaymentDetails {
#IsNumberString()
#IsNotEmpty()
readonly min: string;
#IsNumberString()
#IsNotEmpty()
readonly max: string;
#IsNumberString()
#IsNotEmpty()
readonly percentage: string;
}
Let's say the object being validated is:
{
"payments": false,
"payment_details": {
"min": "1"
}
}
I get no errors although I should because I have set the validator with following flags:
{
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
validationError: {
target: false,
}
}
I am expecting the validator to not allow the child object to be present and throw an error.

One option I've used is to create a custom decorator to validate the nested object since custom decorators work with #ValidateIf()
type Class<T> = { new (...args: any[]): T };
export function ValidateDtoArray(dto: Class<any>, validationOptions?: ValidationOptions) {
return function (object: unknown, propertyName: string) {
registerDecorator({
name: 'validateDto',
target: (object as any).constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
async validate(value: unknown, args: ValidationArguments) {
if (!isArray(value)) return false;
for (let i = 0; i < (value as any[]).length; i++) {
const array_value = (value as any[])[i];
if(typeof array_value !== 'object') return false;
const validation_errors = await validate(plainToClass(dto, array_value as Record<string, unknown>);
if (validation_errors.length > 0) return false;
}
return true;
},
defaultMessage(validationArguments?: ValidationArguments) {
return `Can only be of type ${dto.name}[]`;
},
},
});
};
}
Your code then becomes
export class SuperAdminStoreDto extends StoreDto {
#IsOptional()
#IsBoolean()
readonly payments: boolean;
#ValidateIf(object => object.payments)
#ValidateDtoArray(PaymentDetails)
#Type(() => PaymentDetails)
readonly payment_details: PaymentDetails[];
}

Related

How can I mock a paginated GraphQL query?

I am using apollo/client and graphql-tools/mock to auto mock graphql queries and test React Native components that use them. My schema is generated from an introspection query created by graphql-codegen. For the most part, my queries are getting mocked by addMocksToSchema just fine. However I have a query that is not returning any mock data.
The query is paginated and doesn't follow the same structure of the examples in the docs (https://www.graphql-tools.com/docs/mocking). Instead of having a query with a node that has a field that is a connection type, the connection is returned from the query. This means I can't use relayStylePaginationMock to mock my function because the resolver argument of addMocksToSchema expects the nodes to be objects not functions(function is the return type of relayStylePaginationMock).
In the below code I have tried overriding the newsPost query with a resolver, but I can't figure out how to get the NewsPostEdges from the store and put them in my mock. Everything I have tried has broken the mock and caused it to return undefined for the whole mocked query.
Why does a paginated mock not work by default?
How can I mock this query?
Schema:
type Query {
newsPost: NewsPostConnection
}
type NewsPostConnection {
totalCount: Int
edges: [NewsPostEdge]!
pageInfo: PageInfo!
}
type NewsPostEdge {
node: NewsPostNode
cursor: String!
}
type NewsPostNode {
newsPostId: Int!
isPinned: Boolean!
label: String
title: String
content: String
postType: NewsPostType!
createdDate: DateTime
createdDateTime: String
creator: UserNode!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
endCursor: String
startCursor: String
}
News Posts query:
query NewsPosts(
$after: String
$first: Int
$newsPostId: Filter_ID
$sort: [NewsPostSortEnum]
$isPinned: Filter_Boolean
) {
newsPosts(
after: $after
first: $first
newsPostId: $newsPostId
sort: $sort
isPinned: $isPinned
) {
pageInfo {
hasNextPage
endCursor
}
edges {
post: node {
newsPostId
postType
isPinned
label
createdDateTime
creator {
initials
avatarUrl
displayName
}
content
}
}
}
}
newsPostsContent.test.tsx
import React from 'react';
import { waitFor } from '#testing-library/react-native';
import { PartialDeep } from 'type-fest';
import { faker } from '#faker-js/faker';
import { createFakeUser, render } from '#root/unit-tests/#util';
import { NewsPostNode, NewsPostType } from '#root/src/generated';
import NewsPostContent from '../NewsPostContent';
const mocks = {
NewsPostNode: (): PartialDeep<NewsPostNode> => {
const postId = faker.random.numeric(4);
const createdDate = faker.date.recent(10);
return {
postId,
isPinned: true,
label: 'test',
content: `<div><p>${faker.random.words(10)}</p></div>`,
postType: NewsPostType.Announcement,
createdDate: createdDate.toISOString(),
createdDateTime: createdDate.toISOString(),
};
},
UserNode: createUserPerson(),
};
describe('Dashboard News', () => {
it('renders dashboard news', async () => {
const { getByTestId, debug } = render(
<NewsPostContent />,
mocks,
);
await waitFor(() => [debug(), expect(getByTestId('newsPostContent:Card')).toBeDefined()]);
});
});
NewsPostsContetnt.tsx
const NewsPostContent = () => {
const [newsPostList, setNewsPostList] = useState<PartialDeep<NewsPostNode>[]>([])
const {
data,
loading,
refetch: refetchPosts,
} = useNewsPostsQuery({
variables: { first: MAX_POSTS, isPinned: true, sort: [PostSortEnum.CreatedDateDesc] },
});
console.log(data); // <-- returns undefined when mock breaks
useEffect(() => {
const newsPostEdges = data?.newsPosts?.edges ?? [];
const newsPostNodes = newsPostEdges.reduce((posts, newsPostNode) => {
if (newsPostNode?.post) {
posts.push(newsPostNode.post);
}
return posts;
}, [] as PartialDeep<NewsPostNode>[]);
setNewsPostList(newsPostNodes);
}, [data]);
return (
{<View>
// Component UI to render posts
</View>}
)
}
AutoMockedProvider.tsx
import React from 'react';
import { ApolloProvider, ApolloClient, InMemoryCache } from '#apollo/client';
import { buildClientSchema } from 'graphql';
import {
addMocksToSchema,
createMockStore,
IMocks,
IMockStore,
relayStylePaginationMock,
} from '#graphql-tools/mock';
import { SchemaLink } from '#apollo/client/link/schema';
import { faker } from '#faker-js/faker';
const introspectionResult = require('../../src/generated/introspection.json');
const defaultMocks = {
Date: () => faker.date.recent().toISOString(),
DateTime: () => faker.date.recent().toISOString(),
};
const resolvers = (store: IMockStore) => ({
Query: {
newsPosts: (root, { isPinned, after, first, postId, sort }) => {
return {
edges: (ref) => {
const connectionsRef = store.get('NewsPostConnection');
const edgesRef = store.get(connectionsRef, 'edges');
return edgesRef; // <-- this breaks the mock
},
pageInfo: {
endCursor: null,
hasNextPage: false,
},
};
},
},
});
const AutoMockedProvider = ({
mocks = {},
children,
}: React.PropsWithChildren<{ mocks?: IMocks }>) => {
const schema = buildClientSchema(introspectionResult);
const store = createMockStore({ mocks: { ...defaultMocks, ...mocks }, schema });
const schemaWithMocks = addMocksToSchema({
schema,
mocks: {
...defaultMocks,
...mocks,
},
resolvers,
preserveResolvers: false,
store,
});
const client = new ApolloClient({
link: new SchemaLink({ schema: schemaWithMocks }),
cache: new InMemoryCache(),
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
export default AutoMockedProvider;

Ember sideload data not linked

I'm new to using Ember and was assigned to an ongoing project and need to resolve the following:
export default class OrderModel extends Model.extend(LoadableModel) {
#attr('string') status;
#attr('number') total;
#hasMany('order-item', { async: true }) orderItems;
}
export default class OrderItemModel extends Model.extend(LoadableModel) {
#attr('number', { defaultValue: 0 }) discount;
#attr('number', { defaultValue: 0 }) price;
#hasMany('item-fix', { async: false }) fixes;
}
export default class ItemFixModel extends Model.extend(LoadableModel) {
#attr('number', { defaultValue: 0 }) price;
}
and when I do let order = await this.store.findRecord('order', order_id, { reload: true });
the json response is:
data: {
type: "orders",
id: "1584",
attributes: {
status: "in_progress",
total: 1300
},
relationships: {
order-items: {
data: [
{
type: "order-items",
id: "1801
}
]
}
}
},
included: [
{
type: "order-items"
id: "1801",
attributes: {
discount: 0,
price: 1200
},
relationships: {
item-fixes: {
data: [
{
type: "item-fixes",
id: "335"
}
]
}
},
{
type: "item-fixes",
id: "335",
attributes: {
price: 100
}
}
]
but when I inspect the orderItem inside the order variable, the itemFixes are empty, but the is in the sideload of the response.
¿How can I link this nested relationship?
Also, here is the serializer.
export default DS.JSONAPISerializer.extend({
serialize(snapshot) {
let serialized = this._super(...arguments);
let { adapterOptions } = snapshot;
if (adapterOptions && adapterOptions.addPromotionCode) {
return { code: serialized.data.attributes.code }
}
serialized.included = A([]);
snapshot.eachRelationship((key, relationship) => {
if (relationship.kind === 'belongsTo') {
if (!isBlank(snapshot.belongsTo(key))) {
let node = snapshot.belongsTo(key).record.serialize({ includeId: true }).data;
delete node.relationships;
serialized.included.pushObject(node);
}
} else if (relationship.kind === 'hasMany') {
if (!isBlank(snapshot.hasMany(key))) {
snapshot.hasMany(key).forEach(ele => {
let node = ele.record.serialize({ includeId: true }).data;
delete node.relationships;
serialized.included.pushObject(node);
});
}
}
});
return serialized;
}
});
In your response the relationship name is item-fixes but in your model it's just fixes. It must be the same.

Angular 2: BehaviorSubject/Subject does not work outside constructor

Hi I need pass the data from one component to another, for this I am using the class BehavorSubject(I tried too with Subject class, but doesnt works for me). This is my code:
Home page has a filter, and when is selected a filter, it called the service and it service should change a variable of homePage
HomePage.ts
#Component({
providers: [PatientService],
})
export class HomePage {
subscription: Subscription;
constructor( public auth: AuthService,
public patientService: PatientService) {
this.subscription = this.patientService.nameGiven.subscribe(
nameGiven => {
this.patientsByElement = nameGiven.toString();
});
------ more code---
}
Filtro.ts
export class FiltroPage {
showFilter(filter : FiltroT): void{
... code ...
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.PatientService.getPatientsByTags(this.token,this.filterSelected);
} , 1000);
}
}
patient-service.ts
import { Subject } from 'rxjs/Subject';
import { Observable ,BehaviorSubject } from 'rxjs/Rx';
#Injectable()
export class PatientService {
nameSource = new BehaviorSubject("asd");
nameGiven = this.nameSource.asObservable();
this.nameSource.next('hi!!!'); //**it works but only in the constructor**
this.nameGiven.subscribe(()=>{
console.log("it changed");
});
getPatientsByTags(token: String, tags: Array<string>){
return new Promise(resolve => {
this.http.get(ConnectionParams.DevEnv + ProceduresNames.TagsByPatient + ProceduresNames.TagIdEtiqueta + tags, options)
.map(res => res.json())
.subscribe(data => {
if(data.data.poAnicuRegistros){
console.log("here")
this.nameSource.next('hi TON :/'); // <-- ***here is the problem. It doesnt work***
}
else
console.log("XX");
resolve( this.data);
});
});
}
}
Finally i didn't use the BehaviorSubject/Subject, i pass the data from the filter to Homepage of this way :
HomePage.ts
public popoverCtrl: PopoverController
//code ...
showFilter(myEvent) {
let popover = this.popoverCtrl.create(FiltroPage, {
showConfirm: (x) => {
//do something with the data received from the filter
}
});
popover.present({
ev: myEvent
});
}
Filter.ts
//code...
params: NavParams;
showConfirm() {// function that return the data to homePage
this.params.get('showConfirm')(this.patientsBytag);
}

How to pass an object to Ember.Handlebars.helper

I'm trying to find out how to pass an Ember object through Handlebars to a helper function. Any ideas of how to do this?
Data
groups = [
{
id:'as93-2dsa',
name:'read'
},
{
id:'dk20-d23k',
name: 'write'
},
{
id: 'mn20-8al3',
name: 'execute'
}
];
user = {
id:'lk30-a8bk',
name: 'Seth'
groupIDs:[
'as93-2dsa',
'lk30-a8bk'
]
}
HTML
{{inGroup groups user}}
Controller
Ember.Handlebars.helper('inGroup', function(groups, user) {
/**
* At this point I would expect groups to be and object and user to be an object
* but both return classes
*/
console.log(groups);
//Class {toString: function, __ember1418251045900: "ember646", __nextSuper: undefined, __ember_meta__: Object, constructor: function…}
console.log(groups.toString());
// "<DS.PromiseArray:ember646>"
console.log(user);
//Class {id: "5CF3A051-12B4-436C-B43A-5696F2792B55", store: Class, container: Container, _changesToSync: Object, _deferredTriggers: Array[0]…}
console.log(user.toString());
// "<nfors-app#model:user::ember526:5CF3A051-12B4-436C-B43A-5696F2792B55>"
/**
* I would like to access groups[i].id and user.groupIds in a way such as the following
*/
for (var i = 0; i < groups.length; i++) {
if (user.groupIds.indexof(groups[i].id) !== -1) {
return true;
}
}
return false;
});
User Model
import DS from 'ember-data';
import EmberValidations from 'ember-validations';
export default DS.Model.extend(EmberValidations.Mixin, {
name: DS.attr('string'),
groupIds: DS.hasMany('groupIds', { async: true }),
validations: {
name: {
presence: true,
length: { minimum: 5 }
}
}
});

Detecting change in state for Ember object

I am using Ember-Data and one of my properties is a dictionary data structure. I'd like any update to this dictionary to be converted into an action which sets the parent Model into a "dirty" state.
So here's the config:
Model
export default DS.Model.extend({
// standard types
foo: DS.attr('string'),
bar: DS.attr('number'),
baz: DS.attr('boolean'),
// dictionary (aka, flexible set of name value pairs)
dictionary: DS.attr('object')
});
Transform
export default DS.Transform.extend({
deserialize: function(serialized) {
return Ember.Object.create(serialized);
},
serialize: function(deserialized) {
return deserialized;
}
});
This works and let's assume for a moment that the "dictionary" property is defined as:
{
one: { prop1: foo, prop2: bar, prop3: baz },
two: 2,
three: "howdy",
many: [{},{},{}]
}
This means that an Ember Object has four properties. These properties can be a string, a number, an array, or an object. What I'd like is to have some way of identifying any changes to this underlying basket of attributes so I can propagate that to the Model and have it adjust its state to "dirty".
TL;DR - Working JS Bin example
In order to accomplish this you have to do the following:
1. Deserialize the raw object and all its nested deep properties to Ember Objects so they could be Observable
2. Add observers to your model for all existing keys dynamically on every change of the raw object reference, because it can change its content and scheme.
3. Remove these dynamic observers on every raw object reference change and assign the new ones
4. All dynamic properties changes will set timestamp property so that controllers could listen to it
This is a "Deep" transform I wrote in order to accomplish (1):
// app/transforms/deep.js
export
default DS.Transform.extend({
deserializeRecursively: function(toTraverse) {
var hash;
if (Ember.isArray(toTraverse)) {
return Ember.A(toTraverse.map(function(item) {
return this.deserializeRecursively(item);
}, this));
} else if (!Ember.$.isPlainObject(toTraverse)) {
return toTraverse;
} else {
hash = this.generatePlainObject(Ember.keys(toTraverse), Ember.keys(toTraverse).map(function(key) {
return this.deserializeRecursively(Ember.get(toTraverse, key));
}, this));
return Ember.Object.create(hash);
}
},
deserialize: function(serialized) {
return this.deserializeRecursively(serialized);
},
serialize: function(deserialized) {
return deserialized;
},
generatePlainObject: function(keys, values) {
var ret = {};
keys.forEach(function(key, i) {
ret[key] = values[i];
});
return ret;
}
});
This is a mixin for Models with deep raw objects which accomplish (2) & (3) & (4)
// app/mixins/dynamic-observable.js
export
default Ember.Mixin.create({
propertiesToAnalyze: [],
registerRecursively: function(toTraverse, path, propsToObserve) {
if (Ember.isArray(toTraverse)) {
propsToObserve.addObject(path + '.#each');
if (toTraverse.length > 0) {
this.registerRecursively(toTraverse[0], path + '.#each', propsToObserve);
}
} else if (!(toTraverse instanceof Ember.Object)) {
propsToObserve.addObject(path);
} else {
Ember.keys(toTraverse).forEach(function(propertyName) {
this.registerRecursively(Ember.get(toTraverse, propertyName), path + '.' + propertyName, propsToObserve);
}, this);
}
},
addDynamicObserver: function(propertyNameToAnalyze) {
var propertyToAnalyze = this.get(propertyNameToAnalyze),
propsToObserve = Ember.A([]),
currentDynamicProps = this.get('currentDynamicProps'),
propsToRemove = currentDynamicProps.filter(function(prop) {
return new RegExp('^' + prop + '.').test(prop);
});
propsToRemove.forEach(function(prop) {
Ember.removeObserver(prop, this, dynamicPropertiesObserver)
}, this);
currentDynamicProps.removeObjects(propsToRemove);
this.registerRecursively(propertyToAnalyze, propertyNameToAnalyze, propsToObserve);
propsToObserve.forEach(function(prop) {
Ember.addObserver(this, prop, this, 'dynamicPropertiesObserver');
}, this);
currentDynamicProps.addObjects(propsToObserve);
},
dynamicPropertiesObserver: function(sender, key, value, rev) {
this.set('dynamicPropertyTimestamp', new Date().getTime())
},
addDynamicObservers: function() {
this.get('propertiesToAnalyze').forEach(this.addDynamicObserver, this);
},
init: function() {
this._super();
this.get('propertiesToAnalyze').forEach(function(prop) {
Ember.addObserver(this, prop, this, Ember.run.bind(this, this.addDynamicObserver, prop));
}, this);
},
dynamicPropertyTimestamp: null,
currentDynamicProps: Ember.A([])
});
This is how you use the mixin on a model:
// app/models/some-object.js
import DynamicObservable from 'app/mixins/dynamic-observable';
export
default DS.Model.extend(DynamicObservable, {
dictionary: DS.attr('deep'),
propertiesToAnalyze: ['dictionary']
});
Finally, this is an array controller which its model is an array of some-object models
export
default Ember.ArrayController.extend({
message: '',
observeDictionaries: function() {
this.set('message', 'A dictionary has been changed. change time: ' + new Date().getTime());
}.observes('#each.dynamicPropertyTimestamp')
});