jnwltr / swagger-angular-generator

Generator of API layer in TypeScript for Angular 2+ apps
MIT License
91 stars 46 forks source link
angular api form generator ngrx openapi swagger typescript

Purpose

Generate minimalistic TypeScript API layer for Angular with full type reflection of backend model.

What is generated

Services for back-end / API communication

Interfaces

Forms services for the PUT and POST methods

NGRX modules for endpoints (optional)

Have a look at the demo generated files to get better understanding what is being generated.

Install

npm i swagger-angular-generator

Options

-h - show help -s, --src - source directory -d, --dest - destination directory, default: src/api --no-store - do not generate the ngrx modules -u, --swagger-URL-path - swagger URL path, where the swagger ui documentation can be found; default: /swagger, i.e. the resulting address would be http://example/swagger -o, --omit-version - disables API version information to be generated in comments for each file

Use

Run generator

  1. get the swagger scheme (typically at http(s)://[server]/[app-path]/v2/api/api-docs)
  2. save it to json file in input directory and optionally format it for better diff
  3. run via

    1. directly ./node_modules/.bin/swagger-angular-generator
    2. as module swagger-angular-generator package, npm run generate
      "script": {
        "generate": "swagger-angular-generator -s src/api/scheme.json -d src/api/generated"
        ...
      }
    3. or programatically as a method invocation

      import {generate} from 'swagger-angular-generator';
      // or using CommonJS loader
      const {generate} = require('swagger-angular-generator');
      
      generate('conf/api/api-docs.json', 'src/api');

The resulting API layer contains the following structure in the destination directory:

  1. controllers directory stores services containing all API methods devided by controllers
  2. defs directory stores all response interfaces and enums
  3. store directory has modules, which contain associated form service and NGRX actions, reducers and effects
  4. model.ts file reexports all of them together for a simple access

When updating your code for new backend version, we recommend you to follow these steps:

  1. git diff the changes
  2. run tsc for immediate problems
  3. adjust the code, solve problems
  4. commit

Use

In order to consume generated model, follow the steps 1-9 in the following example to use generated API model.

API service usage in component

// 1. import used response interfaces
import {ItemDto, PageDto} from '[relative-path-to-destination-directory]/model';
// 2. import used API service and optionally param interfaces
import {DataService, MethodParams} from '[relative-path-to-destination-directory]/api/DataService';

@Component({
  ...
  // 3. make the service injectable (can be also imported in the module)
  providers: [DataService],
})
export class MyComponent implements OnInit {
  // 4. declare response object variables based on the generated API interfaces
  public items: ItemDto[] = [];
  public page: PageDto;

  // 5. declare request params based on the generated API interface (all params are passed together in one object)
  private params: MethodParams = {
    page: 0,
    size: 10,
    sort: ['name:asc']
  };

  // 6. inject the API service
  constructor(private dataService: DataService) {}

  public ngOnInit() {
    // 7. the returned observable is fully typed
    this.dataService
      .get(this.params)
      // 8. returned data are fully typed
      .subscribe(data => {
        // 9. assignments are type-checked
        this.items = data.content;
        this.page = data.page;
      });
  }
}

Usage of Forms services

<form [formGroup]="exampleFormService.form" (ngSubmit)="sendForm()" class="full-width">
    <input type="text" name="email" placeholder="email"
           formControlName="email" />
    <button type="submit"
            [disabled]="exampleFormService.form.invalid">Save</button>
</form>

NGRX workflow with generated modules, actions, effects, reducers and form services

Import the generated module
@NgModule({
  imports: [
    ...,
    ExampleModule,
    ...,
  ],
})
export class YourModule {}
Component (created by you)

In the component, send the above created form via sendForm() method. Notice the way a generated anction is dispatched.

import {Component, OnDestroy} from '@angular/core';
import {Store} from '@ngrx/store';
import {takeUntil} from 'rxjs/operators';
import {Subject} from 'rxjs/Subject';
import {ExampleFormService} from '../../generated/store/example/exampleModule/example.service';
import {Start as ExampleStart} from '../../generated/store/example/exampleModule/states/actions';
import {AppState} from '../states/exmaple.models';

@Component({
  selector: 'example-component',
  templateUrl: 'example-component.html',
})
export class ExampleComponent implements OnDestroy {

  constructor(
    public exampleFormService: ExampleFormService,
    private store: Store<AppState>,
  ) {}

  sendForm() {
    this.store.dispatch(new ExampleStart(this.exampleFormService.form.value));
  }

  ngOnDestroy() {
    this.ngDestroy.next();
    this.ngDestroy.complete();
  }
}
Effect (generated)
@Injectable()
export class ExampleEffects {
  CreateProductCategory = createEffect(() => this.storeActions.pipe(
    ofType<actions.Start>(actions.Actions.START),
    switchMap((action: actions.Start) => this.exampleService.exampleEndpointMethod(action.payload)
      .pipe(
        map(result => new actions.Success(result)),
        catchError((error: HttpErrorResponse) => of(new actions.Error(error.message))),
      ),
    ),
  ));  

  constructor(
    private storeActions: Actions,
    private adminproductService: AdminProductService,
  ) {}
}
Reducer (generated)

export const initialExampleState: ExampleState = { data: null, loading: false, error: null, };

export const selectorName = 'Example'; export const getExampleSelector = createFeatureSelector(selectorName);

export function ExampleReducer( state: ExampleState = initialExampleState, action: actions.ExampleAction): ExampleState { switch (action.type) { case actions.Actions.START: return {...state, loading: true, error: null}; case actions.Actions.SUCCESS: return {...state, data: action.payload, loading: false}; case actions.Actions.ERROR: return {...state, error: action.payload, loading: false}; default: return state; } }


##### Component (again)
- the data can be retrieved by subscribing to the store

```typescript
ngOnInit() {
    this.exampleState = this.store.pipe(
      takeUntil(this.ngDestroy),
      select(getExampleSelector));
    // OR
    this.data = this.store.select(s => ExampleState.data)
    this.loading = this.store.select(s => ExampleState.loading)
    this.error = this.store.select(s => ExampleState.error)
}

Assumptions / limitations

  1. swagger file is in version 2 format, it must be json
  2. each endpoint must have a tags attribute defined. In addition, there must be exactly one tag defined. The http methods are grouped to services based on the tags, i.e. if two methods have tag "order", both will be generated inside Order.ts
  3. get and delete methods do not contain body
  4. swagger file should contain values for the keys host and basePath so that each generated service method can contain a link to the swagger UI method reference, e.g. http://example.com/swagger/swagger-ui.html#!/Order/Order
  5. definitions section in swagger file does not contain types with inline definitions, i.e. only named subtypes work

Usage of NGRX modules

Development

Testing

How the testing works

Running the tests

To run client tests in interactive mode:

  1. npm test

Release

Pull requests are welcome!

Please do the following before making a PR:

  1. Build the app and regenerate testing files via npm run build.
  2. Check test pass via npm test.
  3. Check files lint via npm run eslint.