@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 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"}
@@ -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"}
@@ -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"}
@@ -0,0 +1,5 @@
1
+ export * from './errors.js';
2
+ export * from './consts.js';
3
+ export * from './service.js';
4
+ export type * from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,4 @@
1
+ export * from './errors.js';
2
+ export * from './consts.js';
3
+ export * from './service.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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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"}
@@ -0,0 +1,6 @@
1
+ import type { InitializedService } from '@owlmeans/context';
2
+ import type { ModuleHandler } from '@owlmeans/module';
3
+ export interface ApiClient extends InitializedService {
4
+ handler: ModuleHandler;
5
+ }
6
+ //# sourceMappingURL=types.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { AbstractResponse } from '@owlmeans/module';
2
+ import type { AxiosResponse } from 'axios';
3
+ export declare const processResponse: (response: AxiosResponse, reply: AbstractResponse<any>) => void;
4
+ //# sourceMappingURL=handler.d.ts.map
@@ -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,2 @@
1
+ export * from './handler.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from './handler.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1,5 @@
1
+
2
+ export * from './errors.js'
3
+ export * from './consts.js'
4
+ export * from './service.js'
5
+ export type * from './types.js'
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,6 @@
1
+ import type { InitializedService } from '@owlmeans/context'
2
+ import type { ModuleHandler } from '@owlmeans/module'
3
+
4
+ export interface ApiClient extends InitializedService {
5
+ handler: ModuleHandler
6
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export * from './handler.js'
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"}