I'm having an issue with Nest + Swagger. When I open my swagger docs I see all of the endpoints I expect but am having two issues:
When I click on a method it expands all of the methods for that controller
The post method says No parameters despite having a DTO defined for the body
Ultimately I think the issue is: Swagger + Nest is not creating unique operationId's for each method. My understanding is that methods will only fail to get unique operationId's when they are not sufficiently unique: 2 methods with identical call signatures for example.
In the past when I've had issues like this it was either because I was missing the #ApiTags decorator, or I had accidentally included duplicate endpoints.
In general it feels like I missed a step in configuring Swagger, or I didn't set it up properly with Fastify. I installed fastify-swagger but I'm not actually using it anywhere, but according the docs on Nest's site the route for the swagger JSON should be /api/json when using Fastify, which it is for me.
Things that didn't work:
Annotating method with unique #ApiOperation
Adding a addTag to the DocumentBuilder chain
Deleting the swagger-ui-express and #nestjs/platform-express dependencies
Removing all of the Fastify deps and switching to the Express equivalents
Update:
Adding #ApiOperation({ operationId: 'test' }) to a method does fix this, but I was under impression that #nest/swagger did this automatically. Are my methods not unique enough?
main.ts
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // allows automatic serialization
app.enableCors();
const config = new DocumentBuilder().setTitle('PIM API').build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.APP_PORT || 3001);
}
bootstrap();
some controller
#ApiTags('chat-messages')
#Controller('chat-messages')
export class ChatMessagesController {
constructor(
private readonly service: ChatMessagesService,
) {}
#Post()
create(#Body() createChatMessageDto: CreateChatMessageDto) {
return this.service.create(createChatMessageDto);
}
#Get(':stream_id')
findByStreamId(#Param('stream_id') streamId: string) {
return this.service.findByStreamId(streamId);
}
ChatMessageDto
export class CreateChatMessageDto {
constructor(partial: Partial<CreateChatMessageDto>) {
Object.assign(this, partial);
}
#IsString()
value: string;
#IsRFC3339()
timestamp: Date;
#IsNotEmpty()
streamId: string;
}
Swagger JSON
{
"/chat-messages":{
"post":{
"operationId":"ChatMessagesController_",
"parameters":[
],
"responses":{
"201":{
"description":"",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/ChatMessage"
}
}
}
}
},
"tags":[
"chat-messages"
]
}
},
"/chat-messages/{stream_id}":{
"get":{
"operationId":"ChatMessagesController_",
"parameters":[
],
"responses":{
"200":{
"description":"",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/ChatMessage"
}
}
}
}
}
},
"tags":[
"chat-messages"
]
}
}
}
Did you try putting #ApiProperty in your dto ?
Like this:
export class CreateChatMessageDto {
constructor(partial: Partial<CreateChatMessageDto>) {
Object.assign(this, partial);
}
#ApiProperty()
#IsString()
value: string;
#ApiProperty()
#IsRFC3339()
timestamp: Date;
#ApiProperty()
#IsNotEmpty()
streamId: string;
}
This allows Swagger to see the properties.
This is what NestJs recommends in their documentation See here
Related
I'm creating an Application Load Balancer using the AWS CDK v2.
This is my code:
const lb = new elb.ApplicationLoadBalancer(this, 'LB', {
vpc: ec2.Vpc.fromLookup(this, 'vpc-lookup', {
isDefault: true
}),
internetFacing: true
});
const listener = lb.addListener('Listener', {
port: 80,
});
My question is how do I get the URL (DNS name) of the load balancer? I need it in the CDK after to update something
TL;DR The name's actual value is resolved at deploy-time. At synth-time, you can pass loadBalancerDnsName to other constructs and CDK will create the necessary references.
Resource identifiers like DNS addresses are generally known only only at deploy-time. The CDK uses Tokens to "represent values that can only be resolved at a later time in the lifecycle of an app" . ApplicationLoadBalancer's loadBalancerDnsName: string property is one of those properties whose value resolves to a string Token placeholder
at synth-time and an actual value at deploy-time.
Here's an example of passing the loadBalancerDnsName between constructs:
export class AlbStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const alb = new elb.ApplicationLoadBalancer(this, 'MyALB', {
vpc: ec2.Vpc.fromLookup(this, 'DefaultVpc', { isDefault: true }),
});
// WON'T WORK: at synth-time, the name attribute returns a Token, not the expected DNS name
console.log(alb.loadBalancerDnsName); // ${Token[TOKEN.220]}
// WILL WORK - CDK will wire up the token in CloudFormation as
new ssm.StringParameter(this, 'MyAlbDns', {
stringValue: alb.loadBalancerDnsName,
});
}
}
The CDK's CloudFormation template output has Fn::GetAtt placeholder for the DNS name that resolves at deploy-time:
// CDK CloudFormation stack template
// Resources section
"MyAlbDnsFD44EB27": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Value": { "Fn::GetAtt": [ "MyALB911A8556", "DNSName" ] } // this will resolve to the string at deploy
},
"Metadata": {
"aws:cdk:path": "TsCdkPlaygroundAlbStack/MyAlbDns/Resource"
}
},
How i can edit asset name? its doesnt work. Thanks
let assetService = $injector.get(self.ctx.servicesMap.get('assetService'));
let activeID = self.ctx.data[0].datasource.entityId
let tenantId = self.ctx.dashboard.authUser.tenantId
let asset = {
additionalInfo: null,
createdTime: 1599121131415, // временно
customerId: {
entityType: "CUSTOMER",
id: self.ctx.dashboard.authUser.customerId
},
id: {
entityType: "ASSET",
id: activeID
},
label: null,
name: "kuku", // временно
tenantId: {
entityType: "TENANT",
id: tenantId
},
type: "справочник"
}
assetService.saveAsset(asset)
Thingsboard is built using Angular 10 currently See releases. You correctly injected the Angular service 'assetService'. You need to follow the Angular method of subscribing to the observable from assetService.
Calling
assetService.saveAsset(asset)
without subscribing means nothing happens. From the Angular University Blog
The multiple versions of the Angular HTTP module all have an RxJS Observable-based API. This means that the multiple calls to the HTTP module will all return an observable, that we need to subscribe to one way or the other.
So here's the code to 'subscribe' to the observable described above
assetService.saveAsset(asset).subscribe(
(response) => {
console.log(
"saveAsset call Success:",
response);
},
response => {
console.log(
"saveAsset call Error:",
response);
},
() => {
console.log(
"saveAsset observable Complete"
);
});
Let me know if there's a mistake in the code above, I didn't test it. And thanks for your question Anzor - it led me to a solution to make a custom Thingsboard widget along with the Widgets Development Guide.
Instead of Singletons, I want to create dynamically class instances in NestJs.
I found two ways:
1) Directly create the class (ChripSensor is then not #Injectable)
import { ChirpSensor } from './chirp-sensor/chirp-sensor';
#Injectable()
export class SensorsService {
registeredSensors: any;
constructor(
#InjectModel('Sensor') private readonly sensorModel: Model<ISensor>,
private i2cService: I2cService) {
const sensors = this.i2cService.getSensors();
sensors.forEach((sensor) => {this.registeredSensors[sensor._id] = new ChirpSensor({name: sensor.name})});
}
I'm wondering if that is consistent with the DI way of nest.js
2) The second solution would be via a factory, but here I don't know how to pass the options.
export const chirpFactory = {
provide: 'CHIRP_SENSOR',
useFactory: (options) => {
console.log('USING FACTORY CHIRP, options', options)
if (process.env.SIMULATION === 'true') {
return new ChirpSensorMock(options);
}
else {
return new ChirpSensor(options);
}
}
};
Not quite sure how to continue here/ inject the factory properly as the examples create the object in the constructor without options?
Question:
What is the NestJs way to create those class instances?
Edit - for B12Toastr
Module - get the Mock or Original on Compile time
providers: [
{
provide: 'CHIRP_SENSOR',
useValue: process.env.SIMULATION === 'true'
? ChirpSensorMock
: ChirpSensor
},
],
Sensor Service
#Injectable()
export class SensorsService {
registeredSensors: any;
constructor(
#Inject('CHIRP_SENSOR') private ChirpSensorClass: any, // any works but ChirpSensorMock | ChirpSensor not
private i2cService: I2cService
) {
const sensors = this.i2cService.getSensors();
sensors.forEach((sensor) => {this.registeredSensors[sensor._id] = new ChirpSensorClass({name: sensor.name})});
}
You can pass options to your factory via DI via useValue or useClass
providers: [
{
provide: MyOptions,
useValue: options
},
{
provide: 'CHIRP_SENSOR',
useFactory: (options: MyOptions) => {
console.log('USING FACTORY CHIRP, options', options);
if (process.env.SIMULATION === 'true') {
return new ChirpSensorMock(options);
} else {
return new ChirpSensor(options);
}
},
},
],
Alternatively, you could also avoid using a factory altogether and make the decision which class to use at compile time via:
providers: [
{
provide: MyOptions,
useValue: options
},
{
provide: 'CHIRP_SENSOR',
useValue: process.env.SIMULATION === 'true'
? ChirpSensorMock
: ChirpSensor
},
],
or simply:
providers: [
{
provide: MyOptions,
useValue: options
},
{
process.env.SIMULATION === 'true' ? ChirpSensorMock : ChirpSensor
},
],
In case you are not using a factory as described above, you would then inject the options in your ChirpSensor (or the Mocked Sensor)` using typical constructor-based dependency injection:
#Injectable()
export class ChripSensor {
constructor(#inject(MyOptions) private options: MyOptions) {
}
// ...
}
Depending on whether your options are wrapped in a class or a simple object you would either use useValue or useClass. With useClass you have to write less code and do not have to use the #Inject decorator since the class itself is used as DI token. However, it seems if MyOptions is a class, you do not need to use #Inject in any case to inject the dependency because NestJS uses the class as DI token, regardless whether you used useValue or useClass to provide the dependency...
In manifest.json, I have following model definition:
{
"sap.ui5": {
"models": {
"SalesInvoices": {
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultCountMode": "Request"
},
"dataSource": "ZAM_SALES_STATISTICS_CDS",
"preload": true
}
}
}
}
As you can see, SalesInvoices is connected to the OData service.
Now on the onInit function in the controller, I am trying to get Metadata from OData as following:
{ // Controller
onInit: function() {
const oPayerModel = this.getView().getModel("SalesInvoices");
console.log(oPayerModel.getMetadata());
setTimeout(() => {
const oPayerModel = this.getView().getModel("SalesInvoices");
console.log(oPayerModel.getMetadata());
}, 600);
},
// ...
}
As you can see, I have to delay to get the OData instance.
setTimeout is not recommended to use in SAPUI5, how can I do it better?
You can avoid setTimeout, as mentioned in this answer, by using the v2.ODataModel API metadataLoaded instead which returns a promise. The promise is fulfilled once the service metadata is loaded successfully.
onInit: async function() {
const oPayerModel = this.getOwnerComponent().getModel("SalesInvoices");
try {
await oPayerModel.metadataLoaded(true);
const oServiceMetadata = oPayerModel.getServiceMetadata(); // NOT .getMetadata()
// ...
} catch (oError) {/* ... */}
},
About the model being undefined in onInit, here are answers with better explanations:
Nabi's answer
My other answer
I think you are running into the issue I reported some time ago: Component + default OData model: this.getView().getModel() returns undefined in onInit() of controllers:
don't use this.getView().getModel() directly in onInit()
instead use this.getOwnerComponent().getModel() in onInit()
anywhere else in the controller you can use this.getView().getModel()
In your case you should be fine changing the suggestion of #boghyon slightly:
onInit: function() {
const oPayerModel = this.getOwnerComponent().getModel("SalesInvoices");
oPayerModel.metadataLoaded().then(this.onMetadataLoaded.bind(this, oPayerModel));
},
onMetadataLoaded: function(myODataModel) {
const metadata = myODataModel.getServiceMetadata(); // NOT .getMetadata()
// ...
},
This way you can get rid of setTimeout(...).
Given that I have an example Model:
var model = new falcor.Model({
cache: {
userById: {
"1": {
name: "User",
email: "user#email.com"
}
},
users: {
current: null
}
}
});
This is a local model that I'm using for testing purposes, and I would like to implement it on a call to users.login so the user so that I can call:
model.call(['users', 'login'], ['user', 'password'])
I realized that if I do this:
var model = new falcor.Model({
cache: {
userById: {
"1": {
name: "User",
email: "user#email.com"
}
},
users: {
current: null,
login: function(user, password) {
console.log('this code is reached', user, password);
// what to return in order to mutate model?
}
},
}
});
When I do the call it gets there, but I can't figure out how to mutate the model as part of the response; on the server side we return the paths with values and invalidates, and it just works, but here I tried:
// trying returning as a jsonGraph response, don't work
login: function() {
return {
jsonGraph: {
users: {
current: {$type: "ref", value: ['userById', '1']}
}
},
paths: [['users', 'current']]
}
}
// trying returning as a path set mutation list, don't work
login: function() {
return [{path: ['users', 'current'], value: {$type: "ref", value: ['userById', '1']}}]
}
// trying force call to set on the model, don't work
login: function() {
this.set([
{path: ['users', 'current'], value: {$type: "ref", value: ['userById', '1']}}
])
}
// trying using ModelResponse, got an example on some external sources, don't work
login: funtion() {
return new ModelResponse((observer) => {
observer.onNext({
jsonGraph: {
users: {
current: {$type: "ref", value: ['userById', '1']}
}
},
paths: [['users', 'current']]
});
observer.onCompleted();
});
}
Now I don't know what else to try; I need a simple way to declare mutations after a call into a local model, if you know how to solve this, please let me know here.
Thanks.
The client model cache only supports JSONGraph, which b/c it is essentially just JSON with some conventions, doesn't support functions. So, when working with a falcor model cache and no dataSource/middle tier router, it is not possible to implement calls.
This can be kind of annoying when prototyping/testing, as a router is conceptually more difficult than a simple JSON cache object. I ran into this a while ago, so I wrote a dataSource module to support it: falcor-local-datasource. The dataSource is initialized with a graph object that does support function nodes, and as with your above examples, will mutate the graph based on the function's returned JSONGraphEnvelope or an array of PathValues.