I can't get this code to work.
import "reflect-metadata";
export class Castable {
[key: string]: any;
constructor(source: any) {
console.log("source: ");
console.log(source);
Object.getOwnPropertyNames(source).forEach((propertyKey) => {
const designType = Reflect.getMetadata("design:type", this, propertyKey);
const customType = Reflect.getMetadata("custom:type", this, propertyKey);
const type = customType !== undefined ? customType : designType;
this[propertyKey] = this.convert(
source[propertyKey],
propertyKey,
type,
0
);
});
console.log("after constructor this: ");
console.log(this);
}
private convert(
source: any,
propertyKey: string,
type: any,
depth: number
): any {
if (type === undefined) {
return source;
}
switch (type.name) {
case "Number":
return Number(source);
case "String":
return String(source);
case "Boolean":
return source.toString() === "true";
default:
return new type(source);
}
}
}
/** --- TreeRoot --- */
export class MyConfigRoot extends Castable {
result: boolean;
count: number;
}
function init() {
const json = '{"result":true, "count":32}';
let input = JSON.parse(json);
let newR = new MyConfigRoot(input);
console.log("after new: ");
console.log(newR);
}
init();
After getting the type with Reflect.getMetadata, type checking is performed.
This code would result in an empty new object.
> node ./dist/test.js
source:
{ result: true, count: 32 }
after constructor this:
MyConfigRoot { result: true, count: 32 }
after new:
MyConfigRoot { result: undefined, count: undefined }
The assignment in constractor seems to succeed, but actually comes up empty.
It is actually deeper than that, but it is a minimal structure to isolate the problem. Why would it be empty?
This is caused by the useDefineForClassFields compilerOptions, or the lack of it. Because if not manually set, its default value is correlated to target option. From docs:
This flag is used as part of migrating to the upcoming standard version of class fields. TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the upcoming specification has a different runtime behavior to TypeScript’s implementation but the same syntax.
This flag switches to the upcoming ECMA runtime behavior.
Default:
true if target is ES2022 or higher, including ESNext, false otherwise.
Feel free to read the detailed backstory. Short explainer to your case goes like this:
/** --- TreeRoot --- */
class MyConfigRoot extends Castable {
result: boolean; // 👈
count: number;
}
These type-only annotations in TS used to be harmless and have no JS runtime effect. However when useDefineForClassFields is introduced, the compilation result changes.
useDefineForClassFields: true
/** --- TreeRoot --- */
class MyConfigRoot extends Castable {
constructor() {
super(...arguments);
Object.defineProperty(this, "result", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "count", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
}
useDefineForClassFields: false
/** --- TreeRoot --- */
class MyConfigRoot extends Castable {
}
Thus the behavior you observed.
Besides tuning TS compiler option, you can also use the declare keyword in your TS v3.7+ code to fine control the JS compilations.
/** --- TreeRoot --- */
class MyConfigRoot extends Castable {
declare result: boolean; // 👈 declare keyword
count: number;
}
// Now `useDefineForClassFields: true` COMPILES TO:
/** --- TreeRoot --- */
class MyConfigRoot extends Castable {
constructor() {
super(...arguments);
Object.defineProperty(this, "count", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
}
Related
import { Op } from 'sequelize';
export const someFunc = (user_id: string) => {
return {
where: {
[Op.or]: {
private: false,
[Op.and]: {
createdBy: user_id,
private: true
}
}
}
};
};
This was working fine before but after upgrading sequelize to version
6.12.1 or some other issue the famous error :-
ts(4023) Exported variable 'someFunc' has or is using name 'OpTypes.or' from external module "/node_modules/sequelize/lib/operators" but cannot be named.
ts(4023) Exported variable 'someFunc' has or is using name 'OpTypes.and' from external module "/node_modules/sequelize/lib/operators" but cannot be named.
started coming up.
I have the following workaround solutions for it :
import { Op } from 'sequelize';
export const someFunc= (user_id: string) => {
return {
where: {
[Op.or as symbol]: {
private: false,
[Op.and as symbol]: {
createdBy: user_id,
private: true
}
}
}
};
};
import { Op } from 'sequelize';
export const someFunc= (user_id: string):object => {
return {
where: {
[Op.or]: {
private: false,
[Op.and]: {
createdBy: user_id,
private: true
}
}
}
};
};
or defining a interface for the object and using it as the return type
export interface someFuncOptions {
where: {
[Op.or]: {
private: boolean;
[Op.and]: {
createdBy: string;
private: boolean;
};
};
};
}
I tried upgrading sequelize to the latest version as well but it still throws the same error. So I concluded this was a typescript error. My current typescript version for the project is 4.2.3 with sequelize-typescript at version 2.1.0.
Should i use the above solutions or is there some other solution better than them. Also if I use the above solution will I have to use the solution everywhere I use the symbols of Op from sequelize in my code. Currenlty this is the only place the error is popping up where I have used Op.or or other symbols from my code.
I have two validator classes NameMinLengthValidator and NameMaxLengthValidator
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
#ValidatorConstraint({ name: 'name', async: false })
export class NameMinLengthValidator implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
return !!text && 2 <= text.length;
}
defaultMessage(args: ValidationArguments) {
return 'Name must be at least 2 characters.';
}
}
#ValidatorConstraint({ name: 'name', async: false })
export class NameMaxLengthValidator implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
return !!text && text.length <= 12;
}
defaultMessage(args: ValidationArguments) {
return 'Name must be max 12 characters.';
}
}
I have to this in every class where I want to validate these constraints
export class MyRequest {
#Validate(NameMinLengthValidator)
#Validate(NameMaxLengthValidator)
name: string;
}
I want to achieve something similar to this, how can I combine both validators?
export class MyRequestCombined {
#Validate(NameLengthValidator)
name: string;
}
You can use NestJS built-in function to combine multiple decorators.
Example from documentation
import { applyDecorators } from '#nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' }),
);
}
source: https://docs.nestjs.com/custom-decorators
I have some trouble in using spread operator.
when using spread operator, unexpected attribute exists in created object.
Can I filter only defined attribute or remove undefined attribute?
type test = { id: string }
const dump = { id: 'id', name: 'name' };
const testResult: test = { ...dump };
// wanted result { id: 'id' }
console.log('test', testResult); // test { id: 'id', name: 'name' }
I think the issue is not with respect to typescript alone here. Typescript is quite liberal in checking types. In the sense
interface IFoo {}
interface IBar {
name: string
}
// No error will be thrown
const foo: IFoo = { name: 'sam' };
const bar: IBar = { name: 'sam' };
// following line will throw error. As IBar expects the property name.
const justFoo: IBar = { greet: 'hello' };
Here foo can be used as a variable of one of types IFoo or IBar. Either will work. But it's not true in case of justFoo, it can only be IFoo.
This is how you should do it:
type test = { id: string }
const dump = { id: 'id', name: 'name' };
const testResult: test = {id:dump.id}
When I went through Pipes documentation I noticed that I can't make #IsInt() validation for application/x-www-form-urlencoded request correctly, cause all values which I passed I receive as string values.
My request data looks like this
My DTO looks like
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
#IsString()
readonly name: string;
#IsInt()
readonly age: number;
#IsString()
readonly breed: string;
}
Validation pipe contains next code
import { PipeTransform, Pipe, ArgumentMetadata, BadRequestException } from '#nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
#Pipe()
export class ValidationPipe implements PipeTransform<any> {
async transform(value, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.find((type) => metatype === type);
}
}
When I debug this pipe I noticed this state
Where:
value - request body value
object - transformed via class-transformer value
errors - error object
As you can see errors tell to us that age must be an integer number.
How can I pass #IsInt() validation for application/x-www-form-urlencoded request?
Libraries versions:
#nestjs/common#4.6.4
class-transformer#0.1.8
class-validator#0.8.1
P.S: I also create a repository where you can run application to test bug. Required branch how-to-pass-int-validation
UPD: after making changes from accepted answer I faced with problem that I put wrong parsed data to storage. Recorded example
Is it possible to get well parsed createCatDto or what I need to do to save it with correct type structure?
All values from a application/x-www-form-urlencoded request are always strings.
So, you could do the following:
import { Transform } from 'class-transformer';
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
#IsString()
readonly name: string;
#Transform(value => Number.isNan(+value) ? 0 : +value) // this field will be parsed to integer when `plainToClass gets called`
#IsInt()
readonly age: number;
#IsString()
readonly breed: string;
}
Adding #Type(() => Number) resolved the issue for me.
import { Type } from 'class-transformer';
import { IsInt, IsNotEmpty } from 'class-validator';
#IsNotEmpty({ message: '' })
#Type(() => Number)
#IsInt({ message: '' })
project: number;
I need to add a default filter into the DataSourceRequestState so that the database request automatically contains some initial filter value. I'm trying to filter by this.applicationName. Does anyone know how I can add a pre-filter to the grid?
export class ServerCertificationGrid implements OnInit {
public data: GridDataResult;
private filter: CompositeFilterDescriptor;
public state: DataSourceRequestState = {
skip: 0,
take: 20,
filter: filterBy(this.data, { // This also does not work.
logic: 'and',
filters: [
{ field: "name", operator: "contains", value: this.applicationName, ignoreCase: true }
]
})
};
#Input() public applicationName: string;
constructor(private dataService: DataService, private dialogService: DialogService) {
console.log(this.applicationName);
// This does not work because this.state.filter is null.
//let fd: FilterDescriptor = {
// field: "name",
// operator: "contains",
// value: this.applicationName,
// ignoreCase: true
//}
//this.state.filter.filters.push(fd);
this.dataService.getCertificationOverview(this.state)
.subscribe(r => {
console.log(r);
this.data = r;
});
}
Here is the DataService code this component is calling.
public getApplicationServers(state: DataSourceRequestState): Observable<DataResult> {
const queryStr = `${toDataSourceRequestString(state)}`; // Serialize the state
const hasGroups = state.group && state.group.length;
return this.http
.get(`${'api/BircCertificationForm'}?${queryStr}`) // Send the state to the server
.map(response => response.json())
.map(({ Data, Total, AggregateResults }) => // Process the response
(<GridDataResult>{
// If there are groups, convert them to a compatible format
data: hasGroups ? translateDataSourceResultGroups(Data) : Data,
total: Total,
// Convert the aggregates if such exist
//aggregateResult: translateAggregateResults(aggregateResults)
})
)
}
You can bind the filter input of the Grid component and provide an initial filter descriptor, like in the following demo:
Initial filter
<kendo-grid
[data]="gridData"
[pageSize]="state.take"
[skip]="state.skip"
[sort]="state.sort"
[filter]="state.filter"
[sortable]="true"
[pageable]="true"
[filterable]="true"
(dataStateChange)="dataStateChange($event)"
>
...
export class AppComponent {
public state: State = {
skip: 0,
take: 5,
// Initial filter descriptor
filter: {
logic: "and",
filters: [{ field: "ProductName", operator: "contains", value: "Chef" }]
}
};
The filter needs to have the "logic" and "filters" fields only, not to be the result of a filterBy() function call.