Closed rajeget closed 7 years ago
Would this help? https://github.com/swagger-api/swagger-codegen/blob/master/samples/client/petstore/typescript-angular/README.md (which is for angular1)
No @wing328 the link is for angular1 as you said , now to make it compile and work for angular2 . We need a package.json (atleast). I used the reference from (https://github.com/swagger-api/swagger-codegen/tree/master/samples/client/petstore/typescript-angular2/npm) and tried making the required file but its seems I need some help here
cc @Vrolijkx
@rajegit could you describe your problem a little bit more in detail? I didn't go the npm way but put the generated api client directly into my ng project. Works perfectly fine for me.
@sebastianhaas . I did following
This codegen doesn't output an angular 2 project, just the required files for a ng2-compatible api client. To use it, you have to include it like any other typescript library using something like:
import { YourModelApi, YourModel } from './api';
If you want, you can check out how I did it for a recent project of mine: https://github.com/sebastianhaas/medical-appointment-scheduling/tree/master/src
It's a 1:1 fork of the angular2 class startup project, and I just pointed the swagger codegen output to src/app/api/
. Take a look at, for instance, https://github.com/sebastianhaas/medical-appointment-scheduling/blob/master/src/app/appointment/patient.component.ts to see how to import a specific API endpoint.
@rajegit if you still need help using the TS angular2 client, please reply to let us know.
Not to be nitpicky here, but would it be possible to rename API classes as Services, e.g. PetService
, StoreService
etc. instead of PetApi
, StoreApi
etc.? In addition, could we name the corresponding files in lower-dot case, e.g. pet.service.ts
, store.service.ts
etc., in convention with Angular 2 style guide?
I notice that in @sebastianhaas example, they are already renamed. Was it done by hand or in a script?
Take a look at the 2.3.0
branch, it contains a PR where I introduced the changes you requested. Btw if you need a module that is suitable for AOT compiling, I did that already, just need to find time to submit a PR.
@sebastianhaas, thanks - for some reason 2.3.0 build fails for me. I'll probably just stick to 2.2.2 for now, and when 2.3 comes out I'll do a one-time refactoring for my client.
Noo, 2.3.0
works fine, I just built it. Make sure you're having Java >= 1.7, and maybe try running mvn clean package -DskipTests=true
to skip tests. What's your build's error output? Running a build with tests takes about 8 minutes on my machine, without tests it's just about 30 seconds.
I guess it would be possible to provide a 2.3.0
binary release somewhere, but the one-time-refactoring thing doesn't sound good :)
Got it working. I needed to upgrade Maven from 2 to 3, update openjdk-7, and check out 2.3.0 locally. It's build in <2 min (with tests!) and generates the services well! Thanks @sebastianhaas!
Yay, thanks @sebastianhaas ! Your change to 2.3.0 really made my day! Great work!
I would love if someone could help explain how to use the Angular 2 client generated by Swagger @wing328
I have it in my project and that is all good, however, I don't get how to setup the Configuration
class so that my Authorization
headers get set.
I am using a JWT for authentication, but I think it still needs to be put into the ApiKeys
property of the Configuration
class - is that right?
I have the following component:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SelectItem, MenuItem } from 'primeng/primeng';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { AuthService } from '../service/authservice'
import { ReportingService } from '../service/generated/api/reporting.service'
@Component({
templateUrl: './dashboard.component.html',
providers: [ReportingService]
})
export class DashboardComponent implements OnInit, OnDestroy {
private todo: any;
subscription: Subscription;
constructor(private reporting: ReportingService, private auth: AuthService) { }
ngOnInit() {
this.subscription = this.reporting.reportingCountTodo().subscribe(todo => { this.todo = todo; });
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
But I have no idea how to make sure my authorization header gets set when I call reportingCountTodo()
.
Any help is much appreciated.
When you import ApiModule
into your AppModule
, you can use
@NgModule({
imports: [
ApiModule.forConfig({ apiKeys: [...] }),
...
],
...
})
just like using .forRoot(...)
in any other module, as explained here: https://angular.io/guide/ngmodule#configure-core-services-with-coremoduleforroot
Do I have to do the same if I want to apply basic authentication? How can I provide every time the username and the password?
Do I have to do the same if I want to apply basic authentication?
Yes, as far as I can tell:
export interface ConfigurationParameters {
apiKeys?: {[ key: string ]: string};
username?: string;
password?: string;
accessToken?: string;
basePath?: string;
}
Also make sure you run v2.3 (build from sources), not sure if all support made it into the stable branch yet.
How can I provide every time the username and the password?
Not sure what you mean - would it be ok to set username and password once when loading the app, or do you have to change them dynamically?
As I am new to angular 2, I do not understand what is the ApiModule and how through it I can pass the required parameters for authentication.
Not sure what you mean - would it be ok to set username and password once when loading the app, or do you have to change them dynamically?
No I just need to set them once when loading the app
ApiModule is what gets generated by this package (swagger-codegen). So after generation of the code you end up with a folder that contains api.module.ts
as well as individual services under api/
. Instead of importing these services one by one into your AppModule, you can (should) import the entire api.module.ts
as a regular module. This is done only once in the AppModule (and not any other modules, if you have any) because services have to be singletons. Then, to use the service in a component, you just import that service class definition
inside that component (not to be confused with importing the service instance
, which is done only once in AppModule).
Please take a look at https://angular.io/guide/ngmodule to clarify things further.
ApiModule is what gets generated by this package (swagger-codegen). So after generation of the code you end up with a folder that contains api.module.ts as well as individual services under api/. Instead of importing these services one by one into your AppModule, you can (should) import the entire api.module.ts as a regular module. This is done only once in the AppModule (and not any other modules, if you have any) because services have to be singletons.
I used swagger editor for the client generation, so I do not have api.module.ts under api/ directory. I will try again with swagger-codegen.
Please take a look https://angular.io/guide/ngmodule to clarify things further.
thanks a lot
Ah, that's why I recommend building Codegen from branch 2.3. It contains proper structures.
Correction: in the latest builds on the 2.3 branch, they use a different syntax for forConfig
: you now need to provide not the config object itself, but rather a 'config factory' - a function that will return a Configuration object. So, in your app.module.ts
:
export function apiConfig() {
return new Configuration({
username: '...',
password: '...',
basePath: '...',
});
}
...
@NgModule({
imports: [
ApiModule.forConfig(apiConfig),
...
],
...
})
export class AppModule { }
Hope that helps.
Thank you for your answers they helped a lot Regarding this
export function apiConfig() { return new Configuration({ username: '...', password: '...', basePath: '...', }); }
Do you know how can I pass arguments from my login service to this function in order to pass them to ApiModule?
Do you know how can I pass arguments from my login service to this function in order to pass them to ApiModule?
One solution is to store the credentials in localStorage
and then load them from there. A side advantage of that is your auth system may be decoupled from Angular (e.g. you could use a 3rd-party login screen, like those by Auth0 or Stormpath, without "embedding" them in your Angular code). Another advantage is that your login info could be stored reliably across tabs and sessions. That goes with the usual caveat that you need to take precautions against someone stealing that info from the user's browser.
FWIW if you're serious about using Angular as a client for your API, I'd recommend using a temporary token (e.g, JWT) instead of permanent credentials for API authentication. That way, even if someone else manages to access those credentials, they will be of limited use.
I am just writing an api in order to learn angular, so it is not for serious use yet.
I meant something simpler, just how I can get the credentials from login service and inject them to the service that handles the http calls through this factory function
Understood. Then probably passing them through localStorage
is the easiest approach.
@dinvlad I'm using the current 2.3.0-SNAPSHOT version and the generated typescript-angular2 code looks much better than the current stable 2.2.2. Thanks for the hard work!
The problem though that I have is that I can't change the basePath during runtime of the app because every service is setting it in a local (protected) member variable during construction like this:
constructor(protected http: Http, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) {
if (basePath) {
this.basePath = basePath;
}
if (configuration) {
this.configuration = configuration;
this.basePath = basePath || configuration.basePath || this.basePath;
}
}
Because of this the basePath needs to be known before bootstrapping the app and can't be changed any time later. I don't think its good behaviour when every service sets a local copy of the basePath at construction time when there is a configuration object. This leads to inconsistent behaviour too because you can change every configuration parameter on the configuration object but changes to the basePath are silently ignored.
And besides that, I wanted to use the generated code in an ionic project which doesn't have an environment system like in angular cli where the environment variables are changed during build time. This means, that its hard to get configuration parameters for environments before bootstrapping the angular app and I would need to set the basePath during runtime of the app. But this doesn't work without changing the generated code manually after every change in the yaml file.
I don't know if this is the right place to discuss this though. Do you plan to do further changes on the generated code?
@marcorinck I don't really see why you would want to change the URL an Angular REST service is pointing to at runtime. I also don't quite get your concerns re inconsistent behavior. It's just an offer, you don't have to or shouldn't use both ways to set the base path.
The OpaqueToken/InjectionToken is pretty much best practice I guess. This way, Angular's DI system takes care of providing the proper values at the right time, and you have full control at the time a service is instantiated which imho is the correct way to do it. This way, you don't break Angular's coupling principles and make your services easily testable. Just my two cents.
I wanted to use the generated code in an ionic project which doesn't have an environment system like in angular cli where the environment variables are changed during build time
Are you using some bundling tools like webpack or browserify? They usually offer functionality to do what you want to achieve.
@sebastianhaas To set the base path upon bootstrapping of the angular app when services are created I need to have the value already on hand but this isn't the case when you have different environments like dev, qa and prod. In angular-cli you have the environment files which are overwritten on-the-fly during build, so you can import the dev environment in your code but change this with a simple build parameter.
Ionic 2/3 doesn't have an environment system. They use webpack/rollup internally and have a basic dev/prod environment but nothing like angular-cli provides. Most proposals to do this manually, implement some kind of configuration service which then can be injected in other parts of the application. But this can't be use with the ConfigurationFactory currently used in the typescript-angular2 generator, or can it?
I know that I can write my own code and I don't HAVE to use the generated code. I just wanted to give my 2 cents.
But why do you set the basePath on every service locally? Why have a common configuration object where everything can be changed during runtime but (silently) not the basePath?
They use webpack/rollup internally and have a basic dev/prod environment but nothing like angular-cli provides
https://webpack.js.org/plugins/define-plugin/ is what you want. I've posted a link to the famous Angularclass startup repository as well as a little project of mine where I am actively using this swagger codegen in combination with Webpack and Angular.
I know that I can write my own code and I don't HAVE to use the generated code.
That is not what I wanted to say. I wanted to point out that I don't share your concerns regarding inconsistencies, since you're not supposed to use all the different ways this API client provides for setting a base path in parallel but rather stick with one.
But why do you set the basePath on every service locally? Why have a common configuration object where everything can be changed during runtime but (silently) not the basePath?
I don't. Usually you do this during Angular's bootstrapping, but you can configure Injectors in various other scenarios as well, e.g. for unit or e2e tests. https://angular.io/guide/dependency-injection#configuring-the-injector I don't think your question/problem is related to this codegen though.
Maybe I don't make myself clear enough. Lets try again, shall we?
The generator produces this code in api.module.ts:
public static forConfig(configurationFactory: () => Configuration): ModuleWithProviders {
return {
ngModule: ApiModule,
providers: [ {provide: Configuration, useFactory: configurationFactory}]
}
}
You call this in the "imports" array of your application module like this:
ApiModule.forConfig(apiConfig),
The configurationFactory could look like this:
function apiConfig() {
return new Configuration({
username: '...',
password: '...',
basePath: '....',
});
}
As the generated code doesn't provide any dependencies to the factory provider which is used here I can't use my own configurationService here. I would need to change the api.module.ts to something like this:
public static forConfig(configurationFactory: () => Configuration): ModuleWithProviders {
return {
ngModule: ApiModule,
providers: [ {provide: Configuration, useFactory: configurationFactory, deps: [MyConfigurationService]}]
}
}
The configurationFactory could look like this then:
function apiConfig(myConfigurationService) {
return new Configuration({
username: myConfigurationService.username,
password: myConfigurationService.password,
basePath: myConfigurationService.basePath,
});
}
That would indeed solve my problem, but only after a manual change to the generated code.
This doesn't change the problem that I can still inject the Configuration object in my application into other services and components and change ANY configuration during runtime. This would be used instantly but only the change to the basePath would be ignored because that is stored in every generated service locally (outside of the configuration object).
Question is, why do you need to change basePath at runtime? Do you serve API from several hosts? If the only reason is to use different values for different environments, then as @sebastianhaas already mentioned, you can (should) use either DefinePlugin or EnvironmentPlugin for Webpack. Then in your code, just use process.env.API_PATH
or something similar, and pass the necessary value through a regular environmental variable. The plugins will then automatically substitute process.env.API_PATH
with its value at built time.
Question is, why do you need to change basePath at runtime?
For other clients (e.g ruby, php, C#), we allow changing the basePath during runtime in the configuration object/class. One common use case is for testing in QA, e.g changing basePath from https://api.words.com/v1 to https://api-qa.words.com/v1 or https://localhost:8080/v1 for development.
Ok, if basePath changes from one test to another than of course, that helps.
I have a problem with the question: "Why do you want to change the basePath?" because it basically means: "Why do you want to use code we generated for you?"
For every line of code there needs to be a reason why it exists and what purpose it serves. So like in a code review I would like to ask this question: Why can I set the basePath in 3 different ways?
I think it would be best if there was only 1 way to configure code.
Maybe the generated code could generate a base config class with some defaults like the host/baseUrl from the spec file which will be used when I don't provide a configuration myself. If I want to change the defaults I would need to create my own instance or create a sub-class which I then can give the .forRoot()/forConfig() method.
The services shouldn't store values of the configuration locally anymore and only use the provided configuration so that developers can decide themselves if a change of value is necessary/sane at any time they want.
For every line of code there needs to be a reason why it exists and what purpose it serves. So like in a code review I would like to ask this question: Why can I set the basePath in 3 different ways?
Because this is a codegen, so I feel like it is perfectly valid to offer multiple ways to use the generated API client. You don't have to use all of them. But I can see your point, we could think about adding CLI switches so you don't have all three ways compiled in your client.
The services shouldn't store values of the configuration locally anymore and only use the provided configuration
Which basically is what is happening if you're using the InjectionToken.
so that developers can decide themselves if a change of value is necessary/sane at any time they want.
What you want breaks Angular's DI concept. If you need a service with a different configuration go ahead and request a new one with the desired configuration from the DI provider.
Please don't get me wrong here, but I think this is not at all related to swagger codegen but whether you are willing to use Angular in the way it was designed or not :)
https://angular.io/tutorial/toh-pt4 https://angular.io/guide/dependency-injection https://angular.io/guide/lifecycle-hooks https://angular.io/guide/hierarchical-dependency-injection#re-providing-a-service-at-different-levels https://angular.io/guide/hierarchical-dependency-injection#scenario-specialized-providers https://angular.io/guide/testing#test-a-component-with-an-async-service
Great work on the 2.3.0 branch, this is going to be really useful.
I have a similar question to that one from @fluffybonkers . Often in an SPA where you need to access an API based on authentication you login first, then get a token (e.g. JWT Token).
Is it somehow possible to change the configuration dynamicly so that after logging in and receving a JWT token, the API services that has been injected to the components gets updated with the authKeys['Authorizaion'] =
Bearer
Could you write your token to localStorage
? That would allow you to change it on runtime, without reloading and without having to re-create components.
@grEvenX
you can inject the configuration objekt anywhere in your application and can change the apiKeys property even during runtime. Like so:
class MyService {
constructor(private configuration: Configuration, http: Http) {
}
login() {
this.http.post("/login").subscribe((resp) => {
this.configuration.apiKeys["Authorization"] = response.token;
})
}
}
You can change any configuration this way, but not the baseUrl property. I tried to argue on that earlier, but I don't think I got through.
@marcorinck Thanks a bunch! That does the trick just fine :)
I agree that I don't see why you shouldn't be able to set the baseUrl
the same way.
@marcorinck looks like that requires modification of the generated code? Or can that be done outside of the ApiModule? Just curious.
@dinvlad no, this can be done without changing generated code anywhere anytime in your application. (In the 2.3.0-snapshot branch)
Ah, I see - you're passing a statically constructed configuration object through forConfig
? Basically,
@NgModule({
imports: [ ApiModule.forConfig(getApiConfig) ],
})
...
export const apiConfig = new Configuration({
apiKeys: {},
...
});
export function getApiConfig() {
return apiConfig;
}
and then also injecting apiConfig
object into your other services? Seems like a cool idea too.
Yes, exactly this
@marcorinck @grEvenX - I dont see the generated code use configuration.apiKeys anywhere; How does the HTTP request send the apiKeys in the headers?
@scheler this only works in the 2.3.0-SNAPSHOT branch and - I guess - a correct authorization config in the yaml
securityDefinitions:
Bearer:
description: >
jwt
type: apiKey
name: Authorization
in: header
And the request definitions need this:
security:
- Bearer: []
Then the services will have this auto-generated code::
// authentication (Bearer) required
if (this.configuration.apiKeys["Authorization"]) {
headers.set('Authorization', this.configuration.apiKeys["Authorization"]);
}
@marcorinck - thank you, I will try this out.
@marcorinck - that worked fine, thanks. Is there a way to configure swagger-codegen to respond to 401 response with a redirect to a login URL? Or I have to add an HTTP interceptor myself?
@scheler I don't know of any such possibility and I wouldn't expect that as it is project-specific what to do in this case. You have to write the interceptor yourself (I did the same)
Reference https://github.com/swagger-api/swagger-codegen/tree/master/samples/client/petstore/typescript-angular2/npm
These are ts file , how to compile and create a client ?
Please help