@owlmeans/api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +10 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +10 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +18 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +33 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/service.d.ts +10 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +76 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +6 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/handler.d.ts +4 -0
- package/build/utils/handler.d.ts.map +1 -0
- package/build/utils/handler.js +57 -0
- package/build/utils/handler.js.map +1 -0
- package/build/utils/index.d.ts +2 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +2 -0
- package/build/utils/index.js.map +1 -0
- package/package.json +44 -0
- package/src/consts.ts +18 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +5 -0
- package/src/service.ts +105 -0
- package/src/types.ts +6 -0
- package/src/utils/handler.ts +61 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# @owlmeans/api
|
|
2
|
+
|
|
3
|
+
A comprehensive HTTP API client library for OwlMeans Common applications. This package provides a sophisticated API service that integrates with the OwlMeans module system, handles authentication automatically, and provides robust error handling for fullstack applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@owlmeans/api` package is a core networking library in the OwlMeans Common ecosystem that enables seamless communication between client and server components. It provides:
|
|
8
|
+
|
|
9
|
+
- **Module Integration**: Deep integration with OwlMeans Common Module system for type-safe API calls
|
|
10
|
+
- **Automatic Authentication**: Built-in authentication token management and refresh
|
|
11
|
+
- **Error Handling**: Comprehensive error handling with resilient error types
|
|
12
|
+
- **URL Management**: Automatic URL construction from route definitions
|
|
13
|
+
- **Request/Response Processing**: Advanced request transformation and response processing
|
|
14
|
+
- **Security Features**: Integrated security helpers and header management
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @owlmeans/api
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Core Concepts
|
|
23
|
+
|
|
24
|
+
### API Client Service
|
|
25
|
+
|
|
26
|
+
The API client is implemented as an OwlMeans service that can be registered with the context system. It provides a `handler` function that processes module requests and returns appropriate responses.
|
|
27
|
+
|
|
28
|
+
### Module-Based Requests
|
|
29
|
+
|
|
30
|
+
Instead of making direct HTTP calls, the API client works with OwlMeans Common Modules, which define:
|
|
31
|
+
- Route structure and methods
|
|
32
|
+
- Request/response types
|
|
33
|
+
- Parameter validation
|
|
34
|
+
- URL patterns
|
|
35
|
+
|
|
36
|
+
### Automatic Authentication
|
|
37
|
+
|
|
38
|
+
The client automatically handles authentication tokens:
|
|
39
|
+
- Retrieves tokens from the authentication service
|
|
40
|
+
- Includes tokens in request headers
|
|
41
|
+
- Processes token updates from server responses
|
|
42
|
+
- Integrates with the OwlMeans authentication subsystem
|
|
43
|
+
|
|
44
|
+
## API Reference
|
|
45
|
+
|
|
46
|
+
### Types
|
|
47
|
+
|
|
48
|
+
#### `ApiClient`
|
|
49
|
+
The main API client service interface.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
interface ApiClient extends InitializedService {
|
|
53
|
+
handler: ModuleHandler
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Factory Functions
|
|
58
|
+
|
|
59
|
+
#### `createApiService(alias?: string): ApiClient`
|
|
60
|
+
Creates a new API client service.
|
|
61
|
+
|
|
62
|
+
**Parameters:**
|
|
63
|
+
- `alias` (optional): Service alias for registration (defaults to `'web-client'`)
|
|
64
|
+
|
|
65
|
+
**Returns:** Configured ApiClient instance
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
```typescript
|
|
69
|
+
import { createApiService } from '@owlmeans/api'
|
|
70
|
+
|
|
71
|
+
const apiClient = createApiService('my-api-client')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### `appendApiClient<C, T>(ctx: T, alias?: string): T`
|
|
75
|
+
Appends an API client service to an existing context.
|
|
76
|
+
|
|
77
|
+
**Parameters:**
|
|
78
|
+
- `ctx`: OwlMeans context instance
|
|
79
|
+
- `alias` (optional): Service alias (defaults to `'web-client'`)
|
|
80
|
+
|
|
81
|
+
**Returns:** The context with the API client registered
|
|
82
|
+
|
|
83
|
+
**Example:**
|
|
84
|
+
```typescript
|
|
85
|
+
import { appendApiClient } from '@owlmeans/api'
|
|
86
|
+
import { createBasicContext } from '@owlmeans/context'
|
|
87
|
+
|
|
88
|
+
const context = createBasicContext()
|
|
89
|
+
appendApiClient(context, 'api-service')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Constants
|
|
93
|
+
|
|
94
|
+
#### HTTP Status Codes
|
|
95
|
+
```typescript
|
|
96
|
+
const OK = 200
|
|
97
|
+
const CREATED = 201
|
|
98
|
+
const ACCEPTED = 202
|
|
99
|
+
const FINISHED = 204
|
|
100
|
+
const UNAUTHORIZED_ERROR = 401
|
|
101
|
+
const FORBIDDEN_ERROR = 403
|
|
102
|
+
const SERVER_ERROR = 500
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Default Configuration
|
|
106
|
+
```typescript
|
|
107
|
+
const DEFAULT_ALIAS = 'web-client'
|
|
108
|
+
const protocols = ['http', 'https', 'wss', 'ws']
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Error Types
|
|
112
|
+
|
|
113
|
+
The package provides a comprehensive error hierarchy for API-related failures:
|
|
114
|
+
|
|
115
|
+
#### `ApiError`
|
|
116
|
+
Base error class for all API-related errors.
|
|
117
|
+
|
|
118
|
+
#### `ApiClientError`
|
|
119
|
+
General client-side API errors.
|
|
120
|
+
|
|
121
|
+
#### `ServerCrashedError`
|
|
122
|
+
Error indicating server-side crashes (500 errors).
|
|
123
|
+
|
|
124
|
+
#### `ServerAuthError`
|
|
125
|
+
Error for authentication failures (401 errors).
|
|
126
|
+
|
|
127
|
+
## Usage Examples
|
|
128
|
+
|
|
129
|
+
### Basic Setup
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { createBasicContext } from '@owlmeans/context'
|
|
133
|
+
import { appendApiClient } from '@owlmeans/api'
|
|
134
|
+
|
|
135
|
+
// Create context and add API client
|
|
136
|
+
const context = createBasicContext({
|
|
137
|
+
webService: 'api-client'
|
|
138
|
+
})
|
|
139
|
+
appendApiClient(context, 'api-client')
|
|
140
|
+
|
|
141
|
+
// Initialize the context
|
|
142
|
+
await context.init()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Making API Calls through Modules
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { createApiService } from '@owlmeans/api'
|
|
149
|
+
|
|
150
|
+
// Get the API client from context
|
|
151
|
+
const apiClient = context.service('api-client')
|
|
152
|
+
|
|
153
|
+
// Use with a module - the handler processes the request
|
|
154
|
+
const request = {
|
|
155
|
+
alias: 'user-profile',
|
|
156
|
+
params: { userId: '123' },
|
|
157
|
+
query: { include: 'permissions' },
|
|
158
|
+
headers: { 'Accept': 'application/json' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [result, outcome] = await apiClient.handler(request, responseHandler)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Error Handling
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { ApiError, ServerCrashedError, ServerAuthError } from '@owlmeans/api'
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const [result, outcome] = await apiClient.handler(request, response)
|
|
171
|
+
console.log('API call successful:', result)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof ServerCrashedError) {
|
|
174
|
+
console.error('Server crashed:', error.message)
|
|
175
|
+
} else if (error instanceof ServerAuthError) {
|
|
176
|
+
console.error('Authentication failed:', error.message)
|
|
177
|
+
// Redirect to login
|
|
178
|
+
} else if (error instanceof ApiError) {
|
|
179
|
+
console.error('API error:', error.message)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Configuration
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createApiService } from '@owlmeans/api'
|
|
188
|
+
import { createBasicContext } from '@owlmeans/context'
|
|
189
|
+
|
|
190
|
+
const context = createBasicContext({
|
|
191
|
+
webService: 'custom-api',
|
|
192
|
+
// Configure base URLs, security settings, etc.
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const apiClient = createApiService('custom-api')
|
|
196
|
+
context.registerService(apiClient)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Advanced Features
|
|
200
|
+
|
|
201
|
+
### Automatic Token Refresh
|
|
202
|
+
|
|
203
|
+
The API client automatically handles authentication token updates:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// When server responds with TOKEN_UPDATE header,
|
|
207
|
+
// the client automatically updates the auth service
|
|
208
|
+
const response = await apiClient.handler(request, reply)
|
|
209
|
+
// Token is updated in background if provided by server
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Request Transformation
|
|
213
|
+
|
|
214
|
+
The client handles various content types and request transformations:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Automatic form encoding for application/x-www-form-urlencoded
|
|
218
|
+
const request = {
|
|
219
|
+
alias: 'form-submit',
|
|
220
|
+
body: { name: 'John', email: 'john@example.com' },
|
|
221
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Automatic JSON handling
|
|
225
|
+
const jsonRequest = {
|
|
226
|
+
alias: 'api-call',
|
|
227
|
+
body: JSON.stringify({ data: 'value' }),
|
|
228
|
+
headers: { 'content-type': 'application/json' }
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### URL Construction
|
|
233
|
+
|
|
234
|
+
URLs are automatically constructed from module route definitions:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Module defines: /users/:userId/profile
|
|
238
|
+
// Request provides: { userId: '123' }
|
|
239
|
+
// Result: /users/123/profile
|
|
240
|
+
|
|
241
|
+
const request = {
|
|
242
|
+
alias: 'user-profile',
|
|
243
|
+
params: { userId: '123' },
|
|
244
|
+
host: 'api.example.com',
|
|
245
|
+
base: '/v1'
|
|
246
|
+
}
|
|
247
|
+
// Final URL: https://api.example.com/v1/users/123/profile
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Response Processing
|
|
251
|
+
|
|
252
|
+
The client processes various response scenarios:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Success responses (200, 201, 202, 204)
|
|
256
|
+
// Error responses (401, 403, 500, etc.)
|
|
257
|
+
// Custom error marshaling from server
|
|
258
|
+
// Header-based responses for empty bodies
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Integration with OwlMeans Ecosystem
|
|
262
|
+
|
|
263
|
+
The `@owlmeans/api` package integrates with:
|
|
264
|
+
|
|
265
|
+
- **@owlmeans/context**: Service registration and dependency injection
|
|
266
|
+
- **@owlmeans/module**: Module-based request/response handling
|
|
267
|
+
- **@owlmeans/client-route**: URL parameter extraction and route processing
|
|
268
|
+
- **@owlmeans/auth-common**: Automatic authentication integration
|
|
269
|
+
- **@owlmeans/config**: Security helpers and URL construction
|
|
270
|
+
- **@owlmeans/error**: Resilient error handling and marshaling
|
|
271
|
+
- **@owlmeans/client-config**: Configuration management
|
|
272
|
+
|
|
273
|
+
## Security Considerations
|
|
274
|
+
|
|
275
|
+
- All API calls should use HTTPS in production
|
|
276
|
+
- Authentication tokens are automatically managed
|
|
277
|
+
- Request/response data should be validated
|
|
278
|
+
- Error responses may contain sensitive information and should be handled appropriately
|
|
279
|
+
- CORS and CSP headers should be configured properly on the server side
|
|
280
|
+
|
|
281
|
+
## Performance Features
|
|
282
|
+
|
|
283
|
+
- Automatic request cancellation support
|
|
284
|
+
- Efficient parameter extraction and URL construction
|
|
285
|
+
- Minimal overhead for authenticated requests
|
|
286
|
+
- Built-in retry logic through the OwlMeans module system
|
|
287
|
+
|
|
288
|
+
Fixes #32.
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const SERVER_ERROR = 500;
|
|
2
|
+
export declare const UNAUTHORIZED_ERROR = 401;
|
|
3
|
+
export declare const FORBIDDEN_ERROR = 403;
|
|
4
|
+
export declare const OK = 200;
|
|
5
|
+
export declare const CREATED = 201;
|
|
6
|
+
export declare const ACCEPTED = 202;
|
|
7
|
+
export declare const FINISHED = 204;
|
|
8
|
+
export declare const DEFAULT_ALIAS = "web-client";
|
|
9
|
+
export declare const protocols: string[];
|
|
10
|
+
//# sourceMappingURL=consts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,MAAM,CAAA;AAE/B,eAAO,MAAM,kBAAkB,MAAM,CAAA;AAErC,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,eAAO,MAAM,EAAE,MAAM,CAAA;AAErB,eAAO,MAAM,OAAO,MAAM,CAAA;AAE1B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B,eAAO,MAAM,QAAQ,MAAM,CAAA;AAE3B,eAAO,MAAM,aAAa,eAAe,CAAA;AAEzC,eAAO,MAAM,SAAS,UAAiC,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SERVER_ERROR = 500;
|
|
2
|
+
export const UNAUTHORIZED_ERROR = 401;
|
|
3
|
+
export const FORBIDDEN_ERROR = 403;
|
|
4
|
+
export const OK = 200;
|
|
5
|
+
export const CREATED = 201;
|
|
6
|
+
export const ACCEPTED = 202;
|
|
7
|
+
export const FINISHED = 204;
|
|
8
|
+
export const DEFAULT_ALIAS = 'web-client';
|
|
9
|
+
export const protocols = ['http', 'https', 'wss', 'ws'];
|
|
10
|
+
//# sourceMappingURL=consts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;AAE/B,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;AAElC,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,CAAA;AAErB,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAA;AAE1B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAA;AAE3B,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export declare class ApiError extends ResilientError {
|
|
3
|
+
static typeName: string;
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class ApiClientError extends ApiError {
|
|
7
|
+
static typeName: string;
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class ServerCrashedError extends ApiClientError {
|
|
11
|
+
static typeName: string;
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ServerAuthError extends ApiClientError {
|
|
15
|
+
static typeName: string;
|
|
16
|
+
constructor(message?: string);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAa;gBAEhC,OAAO,GAAE,MAAgB;CAGtC;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAmB;gBAEtC,OAAO,GAAE,MAAgB;CAItC;AAED,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,OAAuB,QAAQ,SAA4C;gBAE/D,OAAO,GAAE,MAAgB;CAItC;AAED,qBAAa,eAAgB,SAAQ,cAAc;IACjD,OAAuB,QAAQ,SAAyC;gBAE5D,OAAO,GAAE,MAAgB;CAItC"}
|
package/build/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export class ApiError extends ResilientError {
|
|
3
|
+
static typeName = 'ApiError';
|
|
4
|
+
constructor(message = 'error') {
|
|
5
|
+
super(ApiError.typeName, `api:${message}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ApiClientError extends ApiError {
|
|
9
|
+
static typeName = 'ApiClientError';
|
|
10
|
+
constructor(message = 'error') {
|
|
11
|
+
super(`client:${message}`);
|
|
12
|
+
this.type = ApiClientError.typeName;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ServerCrashedError extends ApiClientError {
|
|
16
|
+
static typeName = `${ApiClientError.typeName}ServerCrashed`;
|
|
17
|
+
constructor(message = 'error') {
|
|
18
|
+
super(`crashed:${message}`);
|
|
19
|
+
this.type = ServerCrashedError.typeName;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ServerAuthError extends ApiClientError {
|
|
23
|
+
static typeName = `${ApiClientError.typeName}ServerAuth`;
|
|
24
|
+
constructor(message = 'error') {
|
|
25
|
+
super(`auth:${message}`);
|
|
26
|
+
this.type = ServerAuthError.typeName;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
ResilientError.registerErrorClass(ApiError);
|
|
30
|
+
ResilientError.registerErrorClass(ApiClientError);
|
|
31
|
+
ResilientError.registerErrorClass(ServerCrashedError);
|
|
32
|
+
ResilientError.registerErrorClass(ServerAuthError);
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAA;IAE5C,YAAY,UAAkB,OAAO;QACnC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC;;AAGH,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,gBAAgB,CAAA;IAElD,YAAY,UAAkB,OAAO;QACnC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;;AAGH,MAAM,OAAO,kBAAmB,SAAQ,cAAc;IAC7C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,eAAe,CAAA;IAE3E,YAAY,UAAkB,OAAO;QACnC,KAAK,CAAC,WAAW,OAAO,EAAE,CAAC,CAAA;QAC3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;;AAGH,MAAM,OAAO,eAAgB,SAAQ,cAAc;IAC1C,MAAM,CAAU,QAAQ,GAAG,GAAG,cAAc,CAAC,QAAQ,YAAY,CAAA;IAExE,YAAY,UAAkB,OAAO;QACnC,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAA;IACtC,CAAC;;AAGH,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA;AACrD,cAAc,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,mBAAmB,YAAY,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { ApiClient } from './types.js';
|
|
3
|
+
import { BasicClientConfig } from '@owlmeans/client-config';
|
|
4
|
+
type Config = BasicClientConfig;
|
|
5
|
+
interface Context<C extends Config = Config> extends BasicContext<C> {
|
|
6
|
+
}
|
|
7
|
+
export declare const createApiService: (alias?: string) => ApiClient;
|
|
8
|
+
export declare const appendApiClient: <C extends Config, T extends Context<C>>(ctx: T, alias?: string) => T;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAM3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAO3D,KAAK,MAAM,GAAG,iBAAiB,CAAA;AAE/B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;CACnE;AAED,eAAO,MAAM,gBAAgB,WAAW,MAAM,KAAmB,SAuEhE,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,UAAS,MAAM,KAAmB,CAU/G,CAAA"}
|
package/build/service.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { assertContext, createService } from '@owlmeans/context';
|
|
2
|
+
import { extractParams } from '@owlmeans/client-route';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { DEFAULT_ALIAS } from './consts.js';
|
|
5
|
+
import { processResponse } from './utils/handler.js';
|
|
6
|
+
import qs from 'qs';
|
|
7
|
+
import { makeSecurityHelper } from '@owlmeans/config';
|
|
8
|
+
import { RouteMethod } from '@owlmeans/route';
|
|
9
|
+
import { DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common';
|
|
10
|
+
export const createApiService = (alias = DEFAULT_ALIAS) => {
|
|
11
|
+
const location = `api.service:${alias}`;
|
|
12
|
+
const client = createService(alias, {
|
|
13
|
+
handler: async (request, reply) => {
|
|
14
|
+
if (request.canceled === true) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const context = assertContext(client.ctx, location);
|
|
18
|
+
const module = context.module(request.alias);
|
|
19
|
+
await module.resolve();
|
|
20
|
+
const route = module.route.route;
|
|
21
|
+
let path = module.getPath();
|
|
22
|
+
const params = extractParams(path);
|
|
23
|
+
path = params.reduce((path, param) => {
|
|
24
|
+
if (request.params[param] == null) {
|
|
25
|
+
throw new SyntaxError(`No value for param ${param}`);
|
|
26
|
+
}
|
|
27
|
+
return path.replace(`:${param}`, `${request.params[param]}`);
|
|
28
|
+
}, path);
|
|
29
|
+
if (route.host == null && request.host == null) {
|
|
30
|
+
throw new SyntaxError(`No host provided in ${module.alias} route`);
|
|
31
|
+
}
|
|
32
|
+
const helper = makeSecurityHelper(context);
|
|
33
|
+
const url = helper.makeUrl(route, path, { host: request.host, base: request.base });
|
|
34
|
+
let transformer = undefined;
|
|
35
|
+
let body = request.body != null && Object.entries((request.headers ?? {})).find(([key, value]) => key.toLowerCase() === 'content-type' && value?.includes('application/x-www-form-urlencoded')) ? qs.stringify(request.body) : request.body;
|
|
36
|
+
// @TODO Probably this hack needs to be made a little bit less dirty
|
|
37
|
+
if (typeof body === 'string' && route.method === RouteMethod.POST) {
|
|
38
|
+
if (Object.keys(request.headers).every(key => key.toLowerCase() !== 'content-type')) {
|
|
39
|
+
request.headers['content-type'] = 'application/json';
|
|
40
|
+
// It fixes the case when final rest api under application/json can't
|
|
41
|
+
// properly accept canonized jsoned string value (put into quotes)
|
|
42
|
+
transformer = data => data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const response = await axios.request({
|
|
46
|
+
url, method: route.method,
|
|
47
|
+
params: request.query,
|
|
48
|
+
data: body,
|
|
49
|
+
headers: request.headers,
|
|
50
|
+
transformRequest: transformer,
|
|
51
|
+
validateStatus: () => true,
|
|
52
|
+
});
|
|
53
|
+
// @TODO Move somewhere else - desirably into auth package via some middleware
|
|
54
|
+
const headers = response.headers;
|
|
55
|
+
if (headers.has(TOKEN_UPDATE) && context.hasService(DEF_AUTH_SRV)) {
|
|
56
|
+
const auth = context.service(DEF_AUTH_SRV);
|
|
57
|
+
const update = headers.get(TOKEN_UPDATE);
|
|
58
|
+
await auth.update(update == null || update === '' ? undefined : update);
|
|
59
|
+
}
|
|
60
|
+
processResponse(response, reply);
|
|
61
|
+
return [reply.error ?? reply.value, reply.outcome];
|
|
62
|
+
}
|
|
63
|
+
}, service => async () => {
|
|
64
|
+
service.initialized = true;
|
|
65
|
+
});
|
|
66
|
+
return client;
|
|
67
|
+
};
|
|
68
|
+
export const appendApiClient = (ctx, alias = DEFAULT_ALIAS) => {
|
|
69
|
+
const service = createApiService(alias);
|
|
70
|
+
ctx.registerService(service);
|
|
71
|
+
if (ctx.cfg.webService == null) {
|
|
72
|
+
ctx.cfg.webService = alias;
|
|
73
|
+
}
|
|
74
|
+
return ctx;
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,WAAW,SAAU,SAAQ,kBAAkB;IACnD,OAAO,EAAE,aAAa,CAAA;CACvB"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAK1C,eAAO,MAAM,eAAe,aAAc,aAAa,SAAS,gBAAgB,CAAC,GAAG,CAAC,SAyCpF,CAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ModuleOutcome } from '@owlmeans/module';
|
|
2
|
+
import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, OK, SERVER_ERROR, UNAUTHORIZED_ERROR } from '../consts.js';
|
|
3
|
+
import { ResilientError } from '@owlmeans/error';
|
|
4
|
+
import { ApiClientError, ServerAuthError, ServerCrashedError } from '../errors.js';
|
|
5
|
+
export const processResponse = (response, reply) => {
|
|
6
|
+
switch (response.status) {
|
|
7
|
+
case OK:
|
|
8
|
+
reply.resolve(response.data, ModuleOutcome.Ok);
|
|
9
|
+
return;
|
|
10
|
+
case ACCEPTED:
|
|
11
|
+
reply.resolve(response.data, ModuleOutcome.Accepted);
|
|
12
|
+
return;
|
|
13
|
+
case CREATED:
|
|
14
|
+
return processEmptyResponse(response, reply, ModuleOutcome.Created);
|
|
15
|
+
case FINISHED:
|
|
16
|
+
return processEmptyResponse(response, reply, ModuleOutcome.Finished);
|
|
17
|
+
default:
|
|
18
|
+
console.error(response.status, response.statusText);
|
|
19
|
+
console.error(response.headers);
|
|
20
|
+
console.error(response.data);
|
|
21
|
+
try {
|
|
22
|
+
if (typeof response.data === 'string') {
|
|
23
|
+
reply.reject(ResilientError.ensure(response.data, true));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`);
|
|
29
|
+
}
|
|
30
|
+
switch (response.status) {
|
|
31
|
+
case SERVER_ERROR:
|
|
32
|
+
reply.reject(new ServerCrashedError());
|
|
33
|
+
return;
|
|
34
|
+
case UNAUTHORIZED_ERROR:
|
|
35
|
+
reply.reject(new ServerAuthError());
|
|
36
|
+
return;
|
|
37
|
+
case FORBIDDEN_ERROR:
|
|
38
|
+
reply.reject(new ApiClientError('forbidden'));
|
|
39
|
+
return;
|
|
40
|
+
default:
|
|
41
|
+
reply.reject(new ApiClientError(`${response.status}`));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const processEmptyResponse = (response, reply, outcome) => {
|
|
47
|
+
if (response.data != null && response.data != '') {
|
|
48
|
+
reply.resolve(response.data, outcome);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (response.headers.toJSON != null && typeof response.headers.toJSON === 'function') {
|
|
52
|
+
reply.resolve(response.headers.toJSON(), outcome);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
reply.resolve(response.headers, outcome);
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 0 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@owlmeans/auth-common": "^0.1.0",
|
|
24
|
+
"@owlmeans/client-config": "^0.1.0",
|
|
25
|
+
"@owlmeans/client-route": "^0.1.0",
|
|
26
|
+
"@owlmeans/config": "^0.1.0",
|
|
27
|
+
"@owlmeans/context": "^0.1.0",
|
|
28
|
+
"@owlmeans/error": "^0.1.0",
|
|
29
|
+
"@owlmeans/module": "^0.1.0",
|
|
30
|
+
"@owlmeans/route": "^0.1.0",
|
|
31
|
+
"axios": "^1.7.7",
|
|
32
|
+
"qs": "^6.13.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/qs": "^6.9.16",
|
|
36
|
+
"nodemon": "^3.1.7",
|
|
37
|
+
"npm-check": "^6.0.1",
|
|
38
|
+
"typescript": "^5.6.3"
|
|
39
|
+
},
|
|
40
|
+
"private": false,
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
export const SERVER_ERROR = 500
|
|
3
|
+
|
|
4
|
+
export const UNAUTHORIZED_ERROR = 401
|
|
5
|
+
|
|
6
|
+
export const FORBIDDEN_ERROR = 403
|
|
7
|
+
|
|
8
|
+
export const OK = 200
|
|
9
|
+
|
|
10
|
+
export const CREATED = 201
|
|
11
|
+
|
|
12
|
+
export const ACCEPTED = 202
|
|
13
|
+
|
|
14
|
+
export const FINISHED = 204
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_ALIAS = 'web-client'
|
|
17
|
+
|
|
18
|
+
export const protocols = ['http', 'https', 'wss', 'ws']
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error'
|
|
2
|
+
|
|
3
|
+
export class ApiError extends ResilientError {
|
|
4
|
+
public static override typeName = 'ApiError'
|
|
5
|
+
|
|
6
|
+
constructor(message: string = 'error') {
|
|
7
|
+
super(ApiError.typeName, `api:${message}`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ApiClientError extends ApiError {
|
|
12
|
+
public static override typeName = 'ApiClientError'
|
|
13
|
+
|
|
14
|
+
constructor(message: string = 'error') {
|
|
15
|
+
super(`client:${message}`)
|
|
16
|
+
this.type = ApiClientError.typeName
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ServerCrashedError extends ApiClientError {
|
|
21
|
+
public static override typeName = `${ApiClientError.typeName}ServerCrashed`
|
|
22
|
+
|
|
23
|
+
constructor(message: string = 'error') {
|
|
24
|
+
super(`crashed:${message}`)
|
|
25
|
+
this.type = ServerCrashedError.typeName
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ServerAuthError extends ApiClientError {
|
|
30
|
+
public static override typeName = `${ApiClientError.typeName}ServerAuth`
|
|
31
|
+
|
|
32
|
+
constructor(message: string = 'error') {
|
|
33
|
+
super(`auth:${message}`)
|
|
34
|
+
this.type = ServerAuthError.typeName
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ResilientError.registerErrorClass(ApiError)
|
|
39
|
+
ResilientError.registerErrorClass(ApiClientError)
|
|
40
|
+
ResilientError.registerErrorClass(ServerCrashedError)
|
|
41
|
+
ResilientError.registerErrorClass(ServerAuthError)
|
package/src/index.ts
ADDED
package/src/service.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { assertContext, createService } from '@owlmeans/context'
|
|
2
|
+
import type { BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { extractParams } from '@owlmeans/client-route'
|
|
4
|
+
import type { ApiClient } from './types.js'
|
|
5
|
+
import axios, { AxiosHeaders } from 'axios'
|
|
6
|
+
import type { AxiosRequestTransformer } from 'axios'
|
|
7
|
+
import type { CommonModule } from '@owlmeans/module'
|
|
8
|
+
import { DEFAULT_ALIAS } from './consts.js'
|
|
9
|
+
import { processResponse } from './utils/handler.js'
|
|
10
|
+
import { BasicClientConfig } from '@owlmeans/client-config'
|
|
11
|
+
import qs from 'qs'
|
|
12
|
+
import { makeSecurityHelper } from '@owlmeans/config'
|
|
13
|
+
import { RouteMethod } from '@owlmeans/route'
|
|
14
|
+
import { DEF_AUTH_SRV, TOKEN_UPDATE } from '@owlmeans/auth-common'
|
|
15
|
+
import type { AuthService } from '@owlmeans/auth-common'
|
|
16
|
+
|
|
17
|
+
type Config = BasicClientConfig
|
|
18
|
+
|
|
19
|
+
interface Context<C extends Config = Config> extends BasicContext<C> {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const createApiService = (alias: string = DEFAULT_ALIAS): ApiClient => {
|
|
23
|
+
const location = `api.service:${alias}`
|
|
24
|
+
const client: ApiClient = createService<ApiClient>(alias, {
|
|
25
|
+
handler: async (request, reply) => {
|
|
26
|
+
if (request.canceled === true) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const context = assertContext<Config, Context>(client.ctx, location)
|
|
30
|
+
const module = context.module<CommonModule>(request.alias)
|
|
31
|
+
await module.resolve()
|
|
32
|
+
const route = module.route.route
|
|
33
|
+
let path = module.getPath()
|
|
34
|
+
const params = extractParams(path)
|
|
35
|
+
path = params.reduce((path, param) => {
|
|
36
|
+
type Key = keyof typeof request.params
|
|
37
|
+
if (request.params[param as Key] == null) {
|
|
38
|
+
throw new SyntaxError(`No value for param ${param}`)
|
|
39
|
+
}
|
|
40
|
+
return path.replace(`:${param}`, `${request.params[param as Key]}`)
|
|
41
|
+
}, path)
|
|
42
|
+
if (route.host == null && request.host == null) {
|
|
43
|
+
throw new SyntaxError(`No host provided in ${module.alias} route`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const helper = makeSecurityHelper(context)
|
|
47
|
+
|
|
48
|
+
const url = helper.makeUrl(route, path, { host: request.host, base: request.base })
|
|
49
|
+
|
|
50
|
+
let transformer: AxiosRequestTransformer | undefined = undefined
|
|
51
|
+
|
|
52
|
+
let body = request.body != null && Object.entries((request.headers ?? {})).find(
|
|
53
|
+
([ key, value ]) =>
|
|
54
|
+
key.toLowerCase() === 'content-type' && value?.includes('application/x-www-form-urlencoded')
|
|
55
|
+
) ? qs.stringify(request.body) : request.body
|
|
56
|
+
|
|
57
|
+
// @TODO Probably this hack needs to be made a little bit less dirty
|
|
58
|
+
if (typeof body === 'string' && route.method === RouteMethod.POST) {
|
|
59
|
+
if (Object.keys(request.headers).every(key => key.toLowerCase() !== 'content-type')) {
|
|
60
|
+
request.headers['content-type'] = 'application/json'
|
|
61
|
+
// It fixes the case when final rest api under application/json can't
|
|
62
|
+
// properly accept canonized jsoned string value (put into quotes)
|
|
63
|
+
transformer = data => data
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await axios.request({
|
|
68
|
+
url, method: route.method,
|
|
69
|
+
params: request.query,
|
|
70
|
+
data: body,
|
|
71
|
+
headers: request.headers,
|
|
72
|
+
transformRequest: transformer,
|
|
73
|
+
validateStatus: () => true,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// @TODO Move somewhere else - desirably into auth package via some middleware
|
|
77
|
+
const headers = response.headers as AxiosHeaders
|
|
78
|
+
if (headers.has(TOKEN_UPDATE) && context.hasService(DEF_AUTH_SRV)) {
|
|
79
|
+
const auth = context.service<AuthService>(DEF_AUTH_SRV)
|
|
80
|
+
const update = headers.get(TOKEN_UPDATE) as string
|
|
81
|
+
await auth.update(update == null || update === '' ? undefined : update)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
processResponse(response, reply)
|
|
85
|
+
|
|
86
|
+
return [ reply.error ?? reply.value, reply.outcome ] as any
|
|
87
|
+
}
|
|
88
|
+
}, service => async () => {
|
|
89
|
+
service.initialized = true
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return client
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const appendApiClient = <C extends Config, T extends Context<C>>(ctx: T, alias: string = DEFAULT_ALIAS): T => {
|
|
96
|
+
const service = createApiService(alias)
|
|
97
|
+
|
|
98
|
+
ctx.registerService(service)
|
|
99
|
+
|
|
100
|
+
if (ctx.cfg.webService == null) {
|
|
101
|
+
ctx.cfg.webService = alias
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ctx
|
|
105
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AbstractResponse } from '@owlmeans/module'
|
|
2
|
+
import { ModuleOutcome } from '@owlmeans/module'
|
|
3
|
+
import type { AxiosResponse } from 'axios'
|
|
4
|
+
import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, OK, SERVER_ERROR, UNAUTHORIZED_ERROR } from '../consts.js'
|
|
5
|
+
import { ResilientError } from '@owlmeans/error'
|
|
6
|
+
import { ApiClientError, ServerAuthError, ServerCrashedError } from '../errors.js'
|
|
7
|
+
|
|
8
|
+
export const processResponse = (response: AxiosResponse, reply: AbstractResponse<any>) => {
|
|
9
|
+
switch (response.status) {
|
|
10
|
+
case OK:
|
|
11
|
+
reply.resolve(response.data, ModuleOutcome.Ok)
|
|
12
|
+
return
|
|
13
|
+
case ACCEPTED:
|
|
14
|
+
reply.resolve(response.data, ModuleOutcome.Accepted)
|
|
15
|
+
return
|
|
16
|
+
case CREATED:
|
|
17
|
+
return processEmptyResponse(response, reply, ModuleOutcome.Created)
|
|
18
|
+
case FINISHED:
|
|
19
|
+
return processEmptyResponse(response, reply, ModuleOutcome.Finished)
|
|
20
|
+
default:
|
|
21
|
+
|
|
22
|
+
console.error(response.status, response.statusText)
|
|
23
|
+
console.error(response.headers)
|
|
24
|
+
console.error(response.data)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (typeof response.data === 'string') {
|
|
28
|
+
reply.reject(ResilientError.ensure(response.data, true))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
console.error(`Server returned an unrecognizable text error: ${response.status} ${response.data}`)
|
|
33
|
+
}
|
|
34
|
+
switch (response.status) {
|
|
35
|
+
case SERVER_ERROR:
|
|
36
|
+
reply.reject(new ServerCrashedError())
|
|
37
|
+
return
|
|
38
|
+
case UNAUTHORIZED_ERROR:
|
|
39
|
+
reply.reject(new ServerAuthError())
|
|
40
|
+
return
|
|
41
|
+
case FORBIDDEN_ERROR:
|
|
42
|
+
reply.reject(new ApiClientError('forbidden'))
|
|
43
|
+
return
|
|
44
|
+
default:
|
|
45
|
+
reply.reject(new ApiClientError(`${response.status}`))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const processEmptyResponse = (response: AxiosResponse, reply: AbstractResponse<any>, outcome: ModuleOutcome) => {
|
|
52
|
+
if (response.data != null && response.data != '') {
|
|
53
|
+
reply.resolve(response.data, outcome)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (response.headers.toJSON != null && typeof response.headers.toJSON === 'function') {
|
|
57
|
+
reply.resolve(response.headers.toJSON(), outcome)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
reply.resolve(response.headers, outcome)
|
|
61
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
7
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
8
|
+
},
|
|
9
|
+
"exclude": [
|
|
10
|
+
"./dist/**/*",
|
|
11
|
+
"./build/**/*",
|
|
12
|
+
"./*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/consts.ts","./src/errors.ts","./src/index.ts","./src/service.ts","./src/types.ts","./src/utils/handler.ts","./src/utils/index.ts"],"version":"5.6.3"}
|