Skip to content

Commit

Permalink
feat: otter sdk training - integration in angular
Browse files Browse the repository at this point in the history
  • Loading branch information
sdo-1A committed Dec 3, 2024
1 parent b1220ab commit 8e22d61
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 6 deletions.
19 changes: 16 additions & 3 deletions apps/showcase/src/assets/trainings/sdk/program.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,34 @@
"htmlContentUrl": "./steps/angular-integration/instructions.md",
"filesConfiguration": {
"name": "angular-integration",
"startingFile": "apps/tutorial-app/src/app/app.component.ts",
"startingFile": "apps/tutorial-app/src/app/app.config.ts",
"urls": [
{
"path": ".",
"contentUrl": "./shared/monorepo-template.json"
},
{
"path": "./libs/sdk",
"contentUrl": "@o3r-training/showcase-sdk/structure/spec.json"
},
{
"path": "./libs/sdk/src",
"contentUrl": "@o3r-training/training-sdk/structure/src.json"
"contentUrl": "@o3r-training/showcase-sdk/structure/src.json"
},
{
"path": "./apps/tutorial-app/src/app",
"contentUrl": "./steps/angular-integration/exercise.json"
}
],
"solutionUrls": [
{
"path": "./apps/tutorial-app/src/app",
"contentUrl": "./steps/angular-integration/solution.json"
}
],
"mode": "interactive",
"commands": [
"npm install --legacy-peer-deps --ignore-scripts --force",
"npm run ng run sdk:build",
"npm run ng run tutorial-app:serve"
]
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="row m-2">
<div class="col">
<button class="btn btn-outline-primary justify-content-center" (click)="getAvailablePets()">Get Available Pets</button>
<table class="table">
<caption align="top">
Pets with the status <b>available</b> from the Swagger Petstore
</caption>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
@for (pet of pets(); track pet.id; let index = $index) {
<tr>
<td>{{ index + 1 }}</td>
<td>{{ pet.name }}</td>
<td>{{ pet.status }}</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col">
<button class="btn btn-outline-primary" (click)="getPetInventory()">Get Inventory</button>
<div>{{petsInventory() | json}}</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { JsonPipe } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ApiFactoryService } from '@o3r/apis-manager';
import { type Pet, PetApi, StoreApi } from 'sdk';

@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
/** Title of the application */
public title = 'tutorial-app';

/* Inject the ApiFactoryService and get the corresponding APIs */
private readonly petApi;
private readonly storeApi;

private readonly petsWritable = signal<Pet[]>([]);
public readonly pets = this.petsWritable.asReadonly();

private readonly petsInventoryWritable = signal<{ [key: string]: number }>({});
public readonly petsInventory = this.petsInventoryWritable.asReadonly();

public async getAvailablePets() {
/* Get the pets whose status is 'available' */
}

public async getPetInventory() {
/* Get the pets inventory */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ApiFetchClient } from '@ama-sdk/client-fetch';
import { ApiClient, PluginRunner, RequestOptions, RequestPlugin } from '@ama-sdk/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ApiManager, ApiManagerModule } from '@o3r/apis-manager';
import { routes } from './app.routes';

class MockInterceptRequest implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: async (data: RequestOptions) => {
const mockData = data.api?.apiName === 'PetApi'
? [{ name : "mockPetName", photoUrls: ["mockPhotoUrl"], status: "available"}]
: { mockPropertyName : "mockPropertyValue"};
const text = JSON.stringify(mockData);
const blob = new Blob([text], { type: 'application/json' });
const basePath = URL.createObjectURL(blob);
return {
method: 'GET',
basePath,
headers: new Headers()
};
}
};
}
}

class RequestAlertPlugin implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: (data: RequestOptions) => {
alert(JSON.stringify(data));
return data;
}
};
}
}

// Default configuration for all the APIs defined in the ApiManager
const apiConfig: ApiClient = new ApiFetchClient(
// Properties of ApiFetchClient
);

// TODO Add the configuration override for a specific API
const apiManager = new ApiManager(apiConfig);

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
// TODO Add the ApiManagerModule
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
When dealing with an Angular project, you need to ensure that your `ApiClient` will be shared across your application.
The Otter framework provides the `ApiManager` service to manage your API collection.

### Objective
- Leverage the `ApiManager` service to access two different clients to retrieve the list of available pets and submit an order for the first pet returned.
- Add a plugin to the `StoreApi` to alert each time a call is sent.

### Prerequisite
- The package `@o3r/apis-manager` needs to be installed (which has already been done for you).

### Exercise

#### Existing plugins
As you can see in the `app.config.ts` file, a plugin `RequestAlertPlugin` has been created which displays an alert box when the API receives a request.
There is also a `MockInterceptRequest` plugin, similar to the one created in the previous step, to mock the request plugin.

#### Integrate the ApiManagerModule with default configuration
First, set the `apiConfig` by adding the properties of `ApiFetchClient` in the existing variable. This default configuration should contain the
`MockInterceptRequest` plugin (similar to the exercise in the previous step).

Next, integrate the `ApiManagerModule` in the providers of your `ApplicationConfig`. You can use the following lines to guide you:
```typescript
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(ApiManagerModule.forRoot(apiManager))]
};
```

Then, checkout the `app.component.ts` file and inject the `ApiFactoryService` to use your unique instance of the `StoreApi` and `PetApi`.
In the existing functions, update the `pets` signal with the result of a call to `findPetsByStatus` and the `petsInventory` signal with the result of `getInventory`.

Now, when clicking the **Get Available Pets** button, your table should be updated with the mock value of available pets and when clicking the **Get Inventory** button,
you should see the mock value of inventory.

#### Override of the default configuration
Let's override the default configuration by updating `apiManager` and configuring it to use the `RequestAlertPlugin` in the `StoreApi`.
You can inspire yourself with the following lines:

```typescript
const apiManager = new ApiManager(apiConfig, {
// Configuration override for a specific API
StoreApi: new ApiFetchClient({
// Properties of ApiFetchClient
})
});
```

Let's see how this new configuration override impacts the default configuration.
When clicking the **Get Available Pets** button, your table should still display the mock value of available pets (without the alert box).
When clicking the **Get Inventory** button, you should see the request to the `StoreApi` logged in the alert box and the actual result displayed in the UI
(not the mock value like before).

We can conclude that the configuration override does not merge the plugins, but replaces them.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { JsonPipe } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ApiFactoryService } from '@o3r/apis-manager';
import { type Pet, PetApi, StoreApi } from 'sdk';

@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
/** Title of the application */
public title = 'tutorial-app';

/* Inject the ApiFactoryService and get the corresponding APIs */
private readonly petApi = inject(ApiFactoryService).getApi(PetApi);
private readonly storeApi = inject(ApiFactoryService).getApi(StoreApi);

private readonly petsWritable = signal<Pet[]>([]);
public readonly pets = this.petsWritable.asReadonly();

private readonly petsInventoryWritable = signal<{ [key: string]: number }>({});
public readonly petsInventory = this.petsInventoryWritable.asReadonly();

public async getAvailablePets() {
/* Get the pets whose status is 'available' */
const availablePets = await this.petApi.findPetsByStatus({status: 'available'});
this.petsWritable.set(availablePets);
}

public async getPetInventory() {
/* Get the pets inventory */
const inventory = await this.storeApi.getInventory({});
this.petsInventoryWritable.set(inventory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ApiFetchClient } from '@ama-sdk/client-fetch';
import { ApiClient, PluginRunner, RequestOptions, RequestPlugin } from '@ama-sdk/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ApiManager, ApiManagerModule } from '@o3r/apis-manager';
import { routes } from './app.routes';

class MockInterceptRequest implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: async (data: RequestOptions) => {
const mockData = data.api?.apiName === 'PetApi'
? [{ name : "mockPetName", photoUrls: ["mockPhotoUrl"], status: "available"}]
: { mockPropertyName : "mockPropertyValue"};
const text = JSON.stringify(mockData);
const blob = new Blob([text], { type: 'application/json' });
const basePath = URL.createObjectURL(blob);
return {
method: 'GET',
basePath,
headers: new Headers()
};
}
};
}
}

class RequestAlertPlugin implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: (data: RequestOptions) => {
alert(JSON.stringify(data));
return data;
}
};
}
}

// Default configuration for all the APIs defined in the ApiManager
const apiConfig: ApiClient = new ApiFetchClient(
// Properties of ApiFetchClient
{
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new MockInterceptRequest()],
fetchPlugins: []
}
);

const apiManager = new ApiManager(apiConfig, {
// Configuration override for a specific API
StoreApi: new ApiFetchClient({
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new RequestAlertPlugin()],
fetchPlugins: []
})
});

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
importProvidersFrom(ApiManagerModule.forRoot(apiManager))
]
};

0 comments on commit 8e22d61

Please sign in to comment.