Validating constructor parameter and lint error - oop

I'm trying to validate a constructor parameter when creating an instance of a class.
The parameter should be an object containing exactly all the properties (of adequate type) as defined in the class.
If this is not the case, I'd like TypeScript to lint the mismatch.
class User {
username: string;
// more properties
constructor(data:object) {
// Check if data Obejct exactly all the class properties and they are of the right type;
// Set instance properties
}
};
// Desired Output
new User(); // "data parameter missing";
new User(45); // "data parameter is not of type object";
new User(); // "username Poperty missing!";
new User({username:"Michael"}); // Valid;
new User({username:43}); // "username is not of type string";
new User({username:"Michael", favoriteFood: "Pizza"}); // "favoriteFood is not a valid property";
tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"module": "es2015",
"lib": [
"es2016.array.include"
],
"downlevelIteration": true,
"strict": true
}
}

The solution is declaring an interface :
interface UserProps {
username: string;
}
class User implements UserProps {
username: string;
// more properties
constructor (data: UserProps) {
// Check if data Obejct exactly all the class properties and they are of the right type;
// Set instance properties
}
}

Related

How do I annotate an endpoint in NestJS for OpenAPI that takes Multipart Form Data

My NestJS server has an endpoint that accepts files and also additional form data
For example I pass a file and a user_id of the file creator in the form.
NestJS Swagger needs to be told explicitly that body contains the file and that the endpoint consumes multipart/form-data this is not documented in the NestJS docs https://docs.nestjs.com/openapi/types-and-parameters#types-and-parameters.
Luckily some bugs led to discussion about how to handle this use case
looking at these two discussions
https://github.com/nestjs/swagger/issues/167
https://github.com/nestjs/swagger/issues/417
I was able to put together the following
I have added annotation using a DTO:
the two critical parts are:
in the DTO add
#ApiProperty({
type: 'file',
properties: {
file: {
type: 'string',
format: 'binary',
},
},
})
public readonly file: any;
#IsString()
public readonly user_id: string;
in the controller add
#ApiConsumes('multipart/form-data')
this gets me a working endpoint
and this OpenAPI Json
{
"/users/files":{
"post":{
"operationId":"UsersController_addPrivateFile",
"summary":"...",
"parameters":[
],
"requestBody":{
"required":true,
"content":{
"multipart/form-data":{
"schema":{
"$ref":"#/components/schemas/UploadFileDto"
}
}
}
}
}
}
}
...
{
"UploadFileDto":{
"type":"object",
"properties":{
"file":{
"type":"file",
"properties":{
"file":{
"type":"string",
"format":"binary"
}
},
"description":"...",
"example":"'file': <any-kind-of-binary-file>"
},
"user_id":{
"type":"string",
"description":"...",
"example":"cus_IPqRS333voIGbS"
}
},
"required":[
"file",
"user_id"
]
}
}
Here is what I find a cleaner Approach:
#Injectable()
class FileToBodyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const req = ctx.getRequest();
if(req.body && req.file?.fieldname) {
const { fieldname } = req.file;
if(!req.body[fieldname]) {
req.body[fieldname] = req.file;
}
}
return next
.handle();
}
}
const ApiFile = (options?: ApiPropertyOptions): PropertyDecorator => (
target: Object, propertyKey: string | symbol
) => {
ApiProperty({
type: 'file',
properties: {
[propertyKey]: {
type: 'string',
format: 'binary',
},
},
})(target, propertyKey);
};
class UserImageDTO {
#ApiFile()
file: Express.Multer.File; // you can name it something else like image or photo
#ApiProperty()
user_id: string;
}
#Controller('users')
export class UsersController {
#ApiBody({ type: UserImageDTO })
// #ApiResponse( { type: ... } ) // some dto to annotate the response
#Post('files')
#ApiConsumes('multipart/form-data')
#UseInterceptors(
FileInterceptor('file'), //this should match the file property name
FileToBodyInterceptor, // this is to inject the file into the body object
)
async addFile(#Body() userImage: UserImageDTO): Promise<void> { // if you return something to the client put it here
console.log({modelImage}); // all the fields and the file
console.log(userImage.file); // the file is here
// ... your logic
}
}
FileToBodyInterceptor and ApiFile are general, I wish they where in the NestJs
You probably need to install #types/multer to have to Express.Multer.File

Set programmatically jsonValidation for dynamic mapping

I am creating a new vscode extension, and I need to extend the standard usage of the jsonValidation system already present in vscode.
Note : I am talking about the system defined in package.json :
"contributes" : {
"languages": [
{
"id" : "yml",
"filenamePatterns": ["module.service"]
},
{
"id" : "json",
"filenamePatterns": ["module.*"]
}
],
"jsonValidation": [
{
"fileMatch": "module.test",
"url": "./resources/test.schema"
}
]
}
Now, I need to create a dynamic mapping, where the json fields filematch/url are defined from some internal rules (like version and other internal stuff). The standard usage is static : one fileMatch -> one schema.
I want for example to read the version from the json file to validate, and set the schema after that :
{
"version" : "1.1"
}
validation schema must be test-schema.1.1 instead of test-schema.1.0
note : The question is only about the modification of the configuration provided by package.json from the extensions.ts
Thanks for the support
** EDIT since the previous solution was not working in all cases
There is one solution to modify the package.json at the activating of the function.
export function activate(context: vscode.ExtensionContext) {
const myPlugin = vscode.extensions.getExtension("your.plugin.id");
if (!myPlugin)
{
throw new Error("Composer plugin is not found...")
}
// Get the current workspace path to found the schema later.
const folderPath = vscode.workspace.workspaceFolders;
if (!folderPath)
{
return;
}
const baseUri : vscode.Uri = folderPath[0].uri;
let packageJSON = myPlugin.packageJSON;
if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation)
{
let jsonValidation = packageJSON.contributes.jsonValidation;
const schemaUri : vscode.Uri = vscode.Uri.joinPath(baseUri, "/schema/value-0.3.0.json-schema");
const schema = new JsonSchemaMatch("value.ospp", schemaUri)
jsonValidation.push(schema);
}
}
And the json schema class
class JsonSchemaMatch
{
fileMatch: string;
url : string;
constructor(fileMatch : string, url: vscode.Uri)
{
this.fileMatch = fileMatch;
this.url = url.path;
}
}
Another important information is the loading of the element of contributes is not reread after modification, for example
class Language
{
id: string;
filenamePatterns : string[];
constructor(id : string, filenamePatterns: string[])
{
this.id = id;
this.filenamePatterns = filenamePatterns;
}
}
if (packageJSON && packageJSON.contributes && packageJSON.contributes.languages)
{
let languages : Language[] = packageJSON.contributes.languages;
for (let language of languages) {
if (language.id == "json") {
language.filenamePatterns.push("test.my-json-type")
}
}
}
This change has no effect, since the loading of file association is already done (I have not dig for the reason, but I think this is the case)
In this case, creating a settings.json in the workspace directory can do the job:
settings.json
{
"files.associations": {
"target.snmp": "json",
"stack.cfg": "json"
}
}
Be aware that the settings.json can be created by the user with legitimate reason, so don't override it, just fill it.

TypeScript Compiler API: Unexpected behavior of getReturnTypeOfSignature() when return type is an array

Suppose the following component class with a public method get_ids() that returns an array of literal values (here: number[]):
class Component {
private _ids: number[];
public get_ids() {
return this._ids;
}
}
The following code works fine as long as the method returns anything else than an array:
const componentSymbol: ts.Symbol = checker.getSymbolAtLocation( componentNode.name ) // componentNode is the AST node of the component class
const componentType: ts.Type = checker.getDeclaredTypeOfSymbol( componentSymbol );
const property: ts.Symbol = componentType.getProperties[1]; // 2nd property represents the method 'get_ids()'
const methodDeclaration = property.getDeclarations()[0] as ts.MethodDeclaration;
const signature = checker.getSignatureFromDeclaration( methodDeclaration );
const returnType = checker.getReturnTypeOfSignature( signature );
However, since get_ids() returns an array, returnType equals:
{
flags: 1,
id: 4,
intrinsicName: "unknown"
}
And there is basically no information that this should be an array of numbers.
Also, the return value of checker.typeToString(checker.getTypeAtLocation(methodDeclaration)) is () => any. I would expect it to be () => number[].
What am I doing wrong here?
I have experimented a bit with the compiler options I have read in from the tsconfig.json file. Turns out that the lib option causes the problem. When commented out, I'm getting the expected result (() => number).
const compilerOptions = {
allowJs: false,
checkJs: false,
diagnostics: false,
inlineSourceMap: true,
inlineSources: true,
jsx: ts.JsxEmit.React,
// lib: [
// // "es5",
// // "dom",
// // "scripthost",
// // "es2015.iterable"
// ],
module: ts.ModuleKind.AMD,
noEmitHelpers: true,
strictNullChecks: false,
tripInternal: true,
target: ts.ScriptTarget.ES5,
downlevelIteration: true,
baseUrl: "./",
pretty: true,
experimentalDecorators: true
};

Extending lib.d.ts interfaces?

I have some code that handles a drop event and checks if I have to deal with dropped files:
public handleDrop(evt: DropEvent): boolean {
var isUpload: boolean; = false;
var dt: DataTransfer = evt.DataTransfer;
dt.types.forEach((type)=>{
// dt.types is of type DOMStringList
if(type === "File"){
isUpload = true;
}
})
return isUpload;
}
It works just as it is supposed to be, but the TS-compiler gives me following error message:
(117,16): error TS2339: Property 'forEach' does not exist on type 'DOMStringList'.
First of all, DataTransfer.types will always be an array, so in my understanding I should have access to its array methods.
I tried to create a workaround for my problem, by merging a custom DOMStringList Interface to the one defined in my lib.d.ts:
declare interface DOMStringList {
forEach(cb: Function): any;
}
IntelliJ recognizes the interface and does not show me an error for dt.types.forEach anymore.
However my ts-compiler still throws an error and does not seem to recognize my merged interface.
What is the proper way to tell my ts-compiler that I should be able to use array functions on an DOMStringList?
The problem occurs for other generic types like FileList as well.
EDIT:
ts.config.ts:
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"noEmitOnError": false,
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
},
"files": [
"app/typings/app.typings.ts",
"typings/tsd.d.ts"
]
}

Define a reusable component

1-I used following code to define a reusable grid,
but when I make instance, no config in class definition either do not have effect of break the code. What is the reason?
3- Is there any restriction in class config declaration?
2- How I can make some default columns in grid class and add some more columns to its objects?
Thanks
Ext.define("IBS.users.Grid", {
extend: "Ext.grid.Panel",
config:{
selType:'checkboxmodel', //not work
dockedItems:[/* items */], //break
multiSelect:true,
features: [
{
groupHeaderTpl: '{name}',
ftype: 'groupingsummary'
},
{
ftype:'filters',
encode: false, // json encode the filter query
local: true
}
],
viewConfig: { //not work
stripeRows: true,
filterable:true,
loadMask: false
},
listeners : {
itemdblclick: function(dv, record, item, index, e) {
console.log(arguments);
}
}
},
constructor:function(config) {
this.callParent(arguments);
this.initConfig(config);
// this.self.instanceCount++;
}
});
1-I used following code to define a reusable grid, but when I make instance, no config in class definition either do not have effect of break the code. What is the reason?
I can answer why your config doesn't have effect. Because config which is being passed into cunstructor is not your default config. You have to apply your default config in order to make default config to have effect:
constructor:function(config) {
config = Ext.applyIf(config, this.config);
this.callParent(arguments);
this.initConfig(config);
}
However, I don't know why dockedItems:[/* items */] breaks the code. Maybe you have syntax or logical errors somewhere within /* items */.
2- How I can make some default columns in grid class and add some more
columns to its objects?
That is easy. Just override your initComponent function:
Ext.define("IBS.users.Grid", {
extend: "Ext.grid.Panel",
// ...
initComponent : function(){
if (!this.columns) {
// default columns:
this.columns = [{
dataIndex: 'dkjgkjd',
// ...
}];
// if we passed extraColumns config
if (this.extraColumns)
for (var i=0; i < this.extraColumns.length; i++)
this.columns.push(this.extraColumns[i]);
}
this.callParent(arguments);
},
// ...
});
3- Is there any restriction in class config declaration?
I'm not aware of any. However, I wouldn't recommend to declare object configs in class definition. For example:
Ext.define("IBS.users.Grid", {
extend: "Ext.grid.Panel",
bbar: Ext.create('Ext.toolbar.Toolbar', // ...
// ...
});
It will be ok with first instance of the class. But when you create the second instance it's bbar refers to the same object as the first instance. And therefore bbar will disappear from the first grid.
Instead declare object configs in initComponent.