MultipleApiVersions with Swagger - swagger

I am trying to enable versioning on a REST API, where the version is specified in the header, as "api-version":2, vendor media type, as "accept: application/vnd.company.resource-v2+json, application/json; version=2", or in a query string "?version=2". The implementation is using IHttpRouteConstraint and RouteFactoryAttribute. This works perfectly. However, Swagger is not able match the right model with the correct versioned document. The operationId is always from the version 1 model.
public class DevicesController : ApiController
{
[HttpGet, RouteVersion( "api/devices", 1, Name = "v1.devices" )]
[ResponseType( typeof( IEnumerable<Models.V1.Device> ) )]
public IHttpActionResult GetV1( )
{
return Ok( new List<Models.V1.Device> { ... } );
}
[HttpGet, RouteVersion( "api/devices", 2, Name = "v2.devices" )]
[ResponseType( typeof( IEnumerable<Models.V2.Device> ) )]
public IHttpActionResult GetV2( )
{
return Ok( new List<Models.V2.Device> { ... } );
}
}
Swagger docs for V2 has the wrong operationId and MIME Types.
"tags":
[
"Devices"
],
"summary": "Get a list of device(s) by the device identifier",
"operationId": "Devices_GetV1",
"consumes": [ ],
"produces":
[
"application/vnd.safe.inventory-v1+json",
"application/json",
"text/json",
"application/vnd.safe.inventory-v1+xml",
"application/xml",
"text/xml"
],
...
Swashbuckle 5.2.2
I have tried customizing the ApiExplorer as descripted in these post:
github.com/domaindrivendev/Swashbuckle/issues/558
github.com/domaindrivendev/Swashbuckle/issues/317
I have also tried building a custom selector.
I also went down this route as well, but I don’t think that is the same issue.
https://azure.microsoft.com/en-us/documentation/articles/app-service-api-dotnet-swashbuckle-customize/
Asking for any direction any help would be appreciated.
Thanks in advance.

Try add Microsoft.AspNet.WebApi.Versioning.ApiExplorer and use apiExplorer as explained here: https://github.com/Microsoft/aspnet-api-versioning/wiki/API-Documentation
This works for me.

We figured out a solution with a custom ApiExplorer as described in http://blog.ulriksen.net/versioned-iapiexplorer
In the Swagger configuration: using MultipleApiVersions
c.MultipleApiVersions( ( apiDesc, version ) =>
{
var versionConstraint = apiDesc.Route.Constraints[ "version" ] as RouteVersionConstraint;
return ( versionConstraint != null ) && versionConstraint.AllowedVersion == version.ConvertTo<int>( );
},
vc =>
{
vc.Version( "2", "API V2" ); //add this line when v2 is released
vc.Version( "1", "API V1" );
} );

Related

Symfony 5 form sanitise user text input by stripping chars

I have a form in symfony 5:
$builder
->add('name',TextType::class,[
'label'=>'Character Name',
'constraints'=>[
new Regex('/[\w\s]+/')
],
'required'=>false,
'attr'=>[
'class'=>'form-control'
],
'label_attr'=>[
'class'=>'form-label'
]
])->add('gender',ChoiceType::class,[
'label'=>'Gender',
'required'=>false,
'choices'=>[
'Any'=>'',
'Male'=>'Male',
'Female'=>'Female',
'Genderless'=>'Genderless',
'Unknown'=>'Unknown'
],
'attr'=>[
'class'=>'form-control'
],
'label_attr'=>[
'class'=>'form-label'
]
])->add('status',ChoiceType::class,[
'label'=>'Status',
'required'=>false,
'choices'=>[
'Any'=>'',
'Alive'=>'Alive',
'Dead'=>'Dead',
'Unknown'=>'unknown'
],
'attr'=>[
'class'=>'form-control'
],
'label_attr'=>[
'class'=>'form-label'
]
])->add('species',ChoiceType::class,[
'label'=>'Species',
'required'=>false,
'choices'=>[
'Any'=>'',
'Human'=>'Human',
'Alien'=>'Alien'
],
'attr'=>[
'class'=>'form-control'
],
'label_attr'=>[
'class'=>'form-label'
]
])->add('submit',SubmitType::class,[
'label'=>'Filter Results',
'attr'=>[
'class'=>'btn btn-primary'
]
]);
What i want to do if possible is use regex to strip special characters from the "name" field after it's submitted so the resulting field value only contains alphanumeric and spaces, so i want to run this on it:
preg_replace('/[^\w\s]/','',$name);
The closest thing i can find to do this is a model transformer but that doesn't really suit this situation as it's just a one way action.
You could use an EventSubscriber, just like Symfony does internally to trim the value in their TextType field (see https://github.com/symfony/symfony/blob/9045ad4bf2837e302e7cdbe41c38f1af33cbe854/src/Symfony/Component/Form/Extension/Core/EventListener/TrimListener.php ):
<?php
namespace App\Form\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class SanitizeListener implements EventSubscriberInterface
{
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
if (!\is_string($data)) {
return;
}
$event->setData(preg_replace('/[^\w\s]/','',$data));
}
public static function getSubscribedEvents(): array
{
return [FormEvents::PRE_SUBMIT => 'preSubmit'];
}
}
Attach the listener to your name field like this:
$builder->get('name')->addEventSubscriber(new SanitizeListener());

Nest + Swagger not recognizing endpoints or parameters

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

Angular 4 & Rails 5 Post Request JSON Formatting

I'm developing an angular app using a rails backend. I'm having problems formatting the parameters hash so rails can use it. The data is a many to many relationship, and the form contains nested attributes. In Rails, my models utilize the accepts_nested_attributes_for helper. I know exactly what format rails expects, but when I make a POST request, there is one minor detail that's off. I'm going to list below two param hashes. One is what Angular produces, and the other is what Rails expects.
What's off about the Angular request is rails expects a deeper layer of nesting in the expense_expense_categories attributes. I've never understood why rails requires it. What angular produces looks logical to me. My question is.. What do I need to do to format the parameters in Angular? Looking at what I have so far, am I doing this in a way that satisfies Angular best practices?
Angular:
{
"expense": {
"date": "2017/4/13",
"check_number": "132",
"debit": "0",
"notes": "har",
"amount": "24",
"payee_id": "334"
},
"expense_expense_categories_attributes": [{
"expense_category_id": "59",
"amount": 12
},
{
"expense_category_id": "62",
"amount": 11
}
]
}
What Rails expects:
{
"expense": {
"date": "2017/12/12",
"check_number": "122",
"debit": "0",
"notes": "har",
"amount": "24",
"payee_id": "334",
"expense_expense_categories_attributes": {
"210212312": {
"expense_category_id": "72",
"amount": "12"
},
"432323432": {
"expense_category_id": "73",
"amount": "12"
}
}
}
}
My code in angular is as follows.
onSubmit() method in component:
onSubmit() {
this.expenseService.addExpense(this.expenseForm.value)
.subscribe(
() => {
this.errorMessage = '';
},
error => {
this.errorMessage = <any>error;
}
);
this.expenseForm.reset();
}
addExpense in my service file:
addExpense(expense: Expense): Observable<any> {
let headers = new Headers({'Content-Type': 'application/json'});
let options = new RequestOptions({headers: headers});
return this.http.post('http://localhost:3000/expenses', expense, options)
.map(
(res: Response) => {
const expenseNew: Expense = res.json();
this.expenses.push(expenseNew);
this.expensesChanged.next(this.expenses.slice());
})
.catch(this.handleError);
}
my main form:
private initForm() {
let expense_expense_categories_attributes = new FormArray([]);
this.expenseForm = this.fb.group({
id: '',
date: '',
amount: '',
check_number: '',
debit: '',
payee_id: '',
notes: '',
expense_expense_categories_attributes: expense_expense_categories_attributes
});
}
My FormArray for nested attributes:
onAddExpenseCategories() {
(<FormArray>this.expenseForm.get('expense_expense_categories_attributes')).push(
new FormGroup({
'expense_category_id': new FormControl(null, Validators.required),
'amount': new FormControl(null, [
Validators.required
])
})
);
}
UPDATE: I was able to get it working, but I had to use a god awful regex to manipulate the request to what I wanted. It was an extremely ugly option so I still need to find a better option. Is there a better way to format JSON Objects and replace the contents? I'm not sure the correct way to do it. Need help.
You need to add the expense_expense_categories to the wrap_parameters like this:
wrap_parameters :expense, include: [:expense_expense_categories_attributes]
Additional attributes must be explicitly added to wrap_parameters as it only wraps attributes of the model itself by default.

Grab test's pass/fail from TestCase via TFS Rest API

I'm trying to grab the Outcome off of a testcase in TFS, looks something like this.
and I can't seem to find a straightforward way to do it. I've tried to grab the workitem directly, query for the property with no success. I was able to use the SDK to get the data (which I'm trying to avoid)
_tfs = new TfsTeamProjectCollection(new Uri(website)) { ClientCredentials = what };
_tfs.EnsureAuthenticated();
var testService = _tfs.GetService<ITestManagementService>();
var aPoint = plan.QueryTestPoints("SELECT * FROM TestPoint WHERE TestCaseId = 10").SingleOrDefault();
console.Write(aPoint.MostRecentResultOutcome);
I have the ID for the testcase from the webhook so that's not a problem. All I want is that "MostRecentResultOutcome". Is there a way to get that data from the REST api in 1 call?
You could also use below REST API which will return a list of test points through test case ID according to your code info:
GET https://Fabrikam-Fiber-inc.VisualStudio.com/DefaultCollection/fabrikam-fiber-tfvc/_apis/test/plans/1/suites/1/points?testcaseid=39&api-version=1.0
Then will get a response with lastTestRun, lastResutl, outcome...
{
"value": [
{
"id": 1,
"url": "https://fabrikam-fiber-inc.visualstudio.com/DefaultCollection/fabrikam-fiber-tfvc/_apis/test/Plans/1/Suites/1/Points/1",
"assignedTo": {
"id": "d291b0c4-a05c-4ea6-8df1-4b41d5f39eff",
"displayName": "Jamal Hartnett"
},
"configuration": {
"id": "2",
"name": "Windows 8"
},
"lastTestRun": {
"id": "28"
},
"lastResult": {
"id": "100000"
},
"outcome": "Passed",
"state": "Completed",
"testCase": {
"id": "39",
"url": null,
"webUrl": null
},
"workItemProperties": [
{
"workItem": {
"key": "Microsoft.VSTS.TCM.AutomationStatus",
"value": "Not Automated"
}
}
]
}
],
"count": 1
}
As Patrick said, you can't as of right now. What I ended up doing its grabbing the ID and System.TeamProject off of the webhook passing that along as such
private TfsTeamProjectCollection _tfs;
private ITestManagementTeamProject _project;
private readonly ITestManagementService _service;
public TfsThing(string instanceUrl, string user, string password)
{
var cred = new VssBasicCredential(user, password);
_tfs = new TfsTeamProjectCollection(new Uri(instanceUrl)) { ClientCredentials = cred };
_tfs.EnsureAuthenticated();
_service = _tfs.GetService<ITestManagementService>();
}
public string GetTestStatus(int id, string projectName)
{
var project = _service.GetTeamProject(projectName);
var result = project.TestResults.ByTestId(id);
return result.LastOrDefault()?.Outcome.ToString();
}
This was the shortest way I found -- may not be the most efficient though
Incase you were wondering, these are the two packages I used
Install-Package Microsoft.TeamFoundationServer.Client
Install-Package Microsoft.TeamFoundationServer.ExtendedClient

JIRA API after POST returns { errorMessages: [ 'Internal server error' ], errors: {} }

I am trying to create a new issue utilizing the JIRA REST API and whenever I try, I get back the following generic error:
{ errorMessages: [ 'Internal server error' ], errors: {} }
I can successfully GET from the API, and the credentials I'm connecting with have full Admin access to JIRA (so it's not an Auth issue), but I get this error every time with POST. Below is a snippet of the JSON data I'm sending. Am I missing anything obvious?
Below is my JavaScript code. Note I'm using jira-connector from npm. (Real domain replaced with mydomain for this sample code)
const JiraClient = require('jira-connector');
const dotenv = require('dotenv').config();
function createNewIssue(fields) {
const encoded = process.env.JIRA_ENCODED_PW;
const jira = new JiraClient({
host: 'mydomain.atlassian.net',
basic_auth: {
base64: encoded
}
});
return new Promise((resolve, reject) => {
jira.issue.createIssue(fields, (error, issue) => {
if (error) {
console.log(error);
reject(error);
} else {
console.log(issue);
resolve(encoded);
}
});
})
}
Below is the JSON that's being passed into fields in the JS above. Note customfield_17300 is a radio button, and customfield_17300 is a multi-select box. For both cases, I've tried using the "id" and also the actual string "name" value. All IDs below were taken straight from a API GET of the same issue in question:
{
"fields": {
"project": {
"id": "13400"
},
"summary": "TEST API TICKET - 01",
"issuetype": {
"id": "11701"
},
"customfield_14804": { "id": "13716" },
"customfield_14607": "Hardware",
"customfield_17300": [
{
"id": "18322"
}
] ,
"customfield_16301": "Customer PO",
"customfield_14800": "LA, California",
"customfield_16302": "FEDEX 234982347g"
}
}
sigh I figured it out... other posts that said this cryptic error was due to a malformed JSON were correct.
In my route, I passed fields as coming from req.body.fields which actually dove into the fields values instead of passing it straight through. This made it so that when the JSON was sent to JIRA the fields outer wrapper was missing. I changed my route to pass along req.body instead of req.body.fields and all was well.
...that was a fun 4 hours...

Resources