snap-on-openapi 1.0.1 → 1.0.3
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/README.md +15 -15
- package/dist/OpenApi.d.ts +23 -23
- package/dist/OpenApi.js +17 -17
- package/dist/OpenApi.test.d.ts +1 -0
- package/dist/OpenApi.test.js +418 -0
- package/dist/README.md +15 -15
- package/dist/index.d.ts +38 -38
- package/dist/index.js +22 -22
- package/dist/services/ClientGenerator/ClientGenerator.d.ts +2 -2
- package/dist/services/ClientGenerator/ClientGenerator.test.d.ts +1 -0
- package/dist/services/ClientGenerator/ClientGenerator.test.js +37 -0
- package/dist/services/ConfigBuilder/ConfigBuilder.d.ts +9 -9
- package/dist/services/ConfigBuilder/ConfigBuilder.js +1 -1
- package/dist/services/ConfigBuilder/ConfigBuilder.test.d.ts +1 -0
- package/dist/services/ConfigBuilder/ConfigBuilder.test.js +207 -0
- package/dist/services/ConfigBuilder/types/DefaultConfig.d.ts +9 -9
- package/dist/services/ConfigBuilder/types/DefaultConfig.js +6 -6
- package/dist/services/ConfigBuilder/types/DefaultConfig.test.d.ts +1 -0
- package/dist/services/ConfigBuilder/types/DefaultConfig.test.js +82 -0
- package/dist/services/ConfigBuilder/types/DefaultErrorMap.d.ts +7 -7
- package/dist/services/ConfigBuilder/types/DefaultErrorMap.js +4 -4
- package/dist/services/ConfigBuilder/types/DefaultRouteContextMap.d.ts +3 -3
- package/dist/services/ConfigBuilder/types/DefaultRouteMap.d.ts +5 -5
- package/dist/services/ConfigBuilder/types/DefaultRouteMap.js +2 -2
- package/dist/services/ConfigBuilder/types/DefaultRouteParamsMap.d.ts +2 -2
- package/dist/services/ConfigBuilder/types/OpenApiConstructor.d.ts +1 -1
- package/dist/services/DescriptionChecker/DescriptionChecker.d.ts +2 -2
- package/dist/services/DescriptionChecker/DescriptionChecker.js +1 -1
- package/dist/services/DescriptionChecker/DescriptionChecker.test.d.ts +1 -0
- package/dist/services/DescriptionChecker/DescriptionChecker.test.js +120 -0
- package/dist/services/DevelopmentUtils/DevelopmentUtils.test.d.ts +1 -0
- package/dist/services/DevelopmentUtils/DevelopmentUtils.test.js +10 -0
- package/dist/services/ExpressWrapper/ExpressWrapper.d.ts +5 -5
- package/dist/services/ExpressWrapper/ExpressWrapper.js +1 -1
- package/dist/services/ExpressWrapper/ExpressWrapper.test.d.ts +1 -0
- package/dist/services/ExpressWrapper/ExpressWrapper.test.js +136 -0
- package/dist/services/ExpressWrapper/types/ExpressApp.d.ts +1 -1
- package/dist/services/ExpressWrapper/types/ExpressHandler.d.ts +2 -2
- package/dist/services/Logger/Logger.d.ts +2 -1
- package/dist/services/Logger/Logger.js +4 -1
- package/dist/services/Logger/Logger.test.d.ts +1 -0
- package/dist/services/Logger/Logger.test.js +113 -0
- package/dist/services/RoutingFactory/RoutingFactory.d.ts +4 -4
- package/dist/services/SchemaGenerator/SchemaGenerator.d.ts +6 -6
- package/dist/services/SchemaGenerator/SchemaGenerator.js +3 -4
- package/dist/services/SchemaGenerator/SchemaGenerator.test.d.ts +1 -0
- package/dist/services/SchemaGenerator/SchemaGenerator.test.js +142 -0
- package/dist/services/TanstackStartWrapper/TanstackStartWrapper.d.ts +4 -4
- package/dist/services/TanstackStartWrapper/TanstackStartWrapper.js +1 -1
- package/dist/services/TanstackStartWrapper/TanstackStartWrapper.test.d.ts +1 -0
- package/dist/services/TanstackStartWrapper/TanstackStartWrapper.test.js +44 -0
- package/dist/services/TestUtils/TestUtils.d.ts +5 -5
- package/dist/services/TestUtils/TestUtils.js +3 -3
- package/dist/services/TestUtils/TestUtils.test.d.ts +1 -0
- package/dist/services/TestUtils/TestUtils.test.js +20 -0
- package/dist/services/ValidationUtils/ValidationUtils.js +3 -3
- package/dist/services/ValidationUtils/ValidationUtils.test.d.ts +1 -0
- package/dist/services/ValidationUtils/ValidationUtils.test.js +165 -0
- package/dist/services/ValidationUtils/transformers/stringBooleanTransformer.test.d.ts +1 -0
- package/dist/services/ValidationUtils/transformers/stringBooleanTransformer.test.js +19 -0
- package/dist/services/ValidationUtils/transformers/stringDateTransformer.test.d.ts +1 -0
- package/dist/services/ValidationUtils/transformers/stringDateTransformer.test.js +13 -0
- package/dist/services/ValidationUtils/transformers/stringNumberTransfromer.test.d.ts +1 -0
- package/dist/services/ValidationUtils/transformers/stringNumberTransfromer.test.js +47 -0
- package/dist/types/AnyRoute.d.ts +1 -1
- package/dist/types/InitialBuilder.d.ts +8 -8
- package/dist/types/Route.d.ts +2 -2
- package/dist/types/RouteMap.d.ts +2 -2
- package/dist/types/Wrappers.d.ts +3 -3
- package/dist/types/config/AnyConfig.d.ts +3 -3
- package/dist/types/config/AnyRouteConfigMap.d.ts +1 -1
- package/dist/types/config/Config.d.ts +10 -8
- package/dist/types/config/ContextParams.d.ts +2 -2
- package/dist/types/config/ErrorConfigMap.d.ts +1 -1
- package/dist/types/config/ErrorResponse.d.ts +1 -1
- package/dist/types/config/Info.d.ts +1 -1
- package/dist/types/config/RouteConfig.d.ts +1 -1
- package/dist/types/config/RouteConfigMap.d.ts +3 -3
- package/dist/types/config/RouteContextMap.d.ts +2 -2
- package/dist/types/config/Server.d.ts +1 -1
- package/dist/types/errors/BuiltInError.d.ts +2 -2
- package/dist/types/errors/BuiltInError.js +1 -1
- package/dist/types/errors/ValidationError.d.ts +2 -2
- package/dist/types/errors/ValidationError.js +2 -2
- package/dist/types/errors/responses/NotFoundErrorResponse.d.ts +1 -1
- package/dist/types/errors/responses/NotFoundErrorResponse.js +1 -1
- package/dist/types/errors/responses/UnknownErrorResponse.d.ts +1 -1
- package/dist/types/errors/responses/UnknownErrorResponse.js +1 -1
- package/dist/types/errors/responses/ValidationErrorResponse.d.ts +2 -2
- package/dist/types/errors/responses/ValidationErrorResponse.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Snap-On OpenAPI
|
|
2
2
|
|
|
3
3
|
Bring a fully-fledged, type-checked API to your app in just 5 minutes.
|
|
4
4
|
|
|
5
|
-
[What is
|
|
5
|
+
[What is Snap-On OpenAPI](#what-is-snap-on-openapi)
|
|
6
6
|
|
|
7
7
|
[Installation](#installation)
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ Bring a fully-fledged, type-checked API to your app in just 5 minutes.
|
|
|
21
21
|
- TypeScript client generator included
|
|
22
22
|
- Since you have OpenAPI, you can generate clients for any language you want
|
|
23
23
|
|
|
24
|
-
## What is
|
|
24
|
+
## What is Snap-On OpenAPI?
|
|
25
25
|
|
|
26
26
|
OpenAPI is a standard for documenting your REST APIs. It's similar to JSDoc generators but with one major difference: it uses schemas that can be strictly typed and used for code generation.
|
|
27
27
|
|
|
@@ -35,14 +35,14 @@ Zod works so well that I stopped using classes for models and DTOs in my own pro
|
|
|
35
35
|
|
|
36
36
|
You might have the same idea I had some time ago: why not combine Zod and OpenAPI and make our API absolutely type-checked both on the frontend and backend? That would be a blast!
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Snap-On OpenAPI is a lightweight, non-opinionated framework that allows you to do exactly that. It's highly customizable and easy to use, while providing fully type-checked context. You can forget about those "any" types that pop up here and there in your APIs.
|
|
39
39
|
|
|
40
40
|
The framework doesn't have any predefined middlewares (I don't even use such a concept) or excessive code. It has a few built-in errors and validators which I found helpful, and even those are made with the same utilities that are available to you.
|
|
41
41
|
|
|
42
|
-
Simply put,
|
|
42
|
+
Simply put, Snap-On OpenAPI is the glue that ties together OpenAPI, Zod, and Openapi-TS. And you are in charge of how your API is shaped—that's what sets this framework apart from tools like `GraphQL` and `tRPC`.
|
|
43
43
|
|
|
44
44
|
You can check out some sample code here:
|
|
45
|
-
https://github.com/Freddis/
|
|
45
|
+
https://github.com/Freddis/snap-on-openapi-samples
|
|
46
46
|
|
|
47
47
|
## Disclaimer
|
|
48
48
|
Configuration is a bit clunky due to the huge amount of inferred types. But trust me, when you learn the basics (and there is no advanced level—it's really lightweight) you will be able to configure your API in just 5 minutes.
|
|
@@ -70,13 +70,13 @@ export const upsertWorkouts = openApi.factory.createRoute({
|
|
|
70
70
|
## Installation
|
|
71
71
|
|
|
72
72
|
```shell
|
|
73
|
-
npm install
|
|
73
|
+
npm install snap-on-openapi
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
## Quick Start
|
|
77
|
-
The idea behind
|
|
77
|
+
The idea behind Snap-On OpenAPI is that you don't need to bother with configuration right away. It is designed to be configured as you go. Fire it up, focus on your business logic, then add errors, routes, and contexts as you go.
|
|
78
78
|
|
|
79
|
-
Right now,
|
|
79
|
+
Right now, Snap-On OpenAPI provides quickstart wrappers for Tanstack Start and Express.
|
|
80
80
|
|
|
81
81
|
### Express
|
|
82
82
|
|
|
@@ -183,7 +183,7 @@ As you can see, it's not rocket science to integrate it with any framework.
|
|
|
183
183
|
|
|
184
184
|
## Adding Routes
|
|
185
185
|
|
|
186
|
-
Now let's get deeper with
|
|
186
|
+
Now let's get deeper with Snap-On OpenAPI. Let's add some hot action.
|
|
187
187
|
|
|
188
188
|
We need to create a route and then add it to our OpenAPI instance. I recommend using a separate file for each route and one more file for the route map:
|
|
189
189
|
```typescript
|
|
@@ -229,7 +229,7 @@ export const getCars = openapi.factory.createRoute({
|
|
|
229
229
|
Now let's create a route map:
|
|
230
230
|
```typescript
|
|
231
231
|
// file: src/openapi/routes.ts
|
|
232
|
-
import {OpenApiRouteMap, OpenApiSampleRouteType} from '
|
|
232
|
+
import {OpenApiRouteMap, OpenApiSampleRouteType} from 'snap-on-openapi';
|
|
233
233
|
import {getCars} from './getCars';
|
|
234
234
|
|
|
235
235
|
export const openApiRoutes: OpenApiRouteMap<OpenApiSampleRouteType> = {
|
|
@@ -313,9 +313,9 @@ And you can always write your own wrapper function to make it even less verbose
|
|
|
313
313
|
|
|
314
314
|
## Configuration
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
Snap-On OpenAPI comes with a default configuration that covers basic errors and provides public route types. You can start with that, but eventually you will grow out of it.
|
|
317
317
|
|
|
318
|
-
There are two ways to configure
|
|
318
|
+
There are two ways to configure Snap-On OpenAPI:
|
|
319
319
|
1. Inferred config (recommended at the beginning)
|
|
320
320
|
2. Implement OpenApiConfig interface
|
|
321
321
|
|
|
@@ -480,7 +480,7 @@ With that said, you can see that you don't have to list all possible kinds of er
|
|
|
480
480
|
|
|
481
481
|
> [!NOTE]
|
|
482
482
|
> Every error that can be thrown corresponds to one or multiple error responses. Whatever happens during API call processing, the consumer will
|
|
483
|
-
> always receive a response. That's why
|
|
483
|
+
> always receive a response. That's why Snap-On OpenAPI requires at least one error response to be defined: it has to have the default error.
|
|
484
484
|
|
|
485
485
|
The `customizeErrors()` call will set you on the path of configuring errors. Similar to `customizeRoutes()`, you won't be able to call `create()` until you have provided everything required for the API to function properly.
|
|
486
486
|
|
|
@@ -538,7 +538,7 @@ export const openApi = OpenApi.builder.customizeErrors(
|
|
|
538
538
|
> [!NOTE]
|
|
539
539
|
> Note that the interface of `defineDefaultError` forces you to use synchronous context. It's no coincidence: errors may happen during your own error handling. This approach guarantees that whatever happens, we always have a suitable response ready.
|
|
540
540
|
|
|
541
|
-
The last thing we need to do is write the error handler itself.
|
|
541
|
+
The last thing we need to do is write the error handler itself. Snap-On OpenAPI can't magically know what error to respond with; the best it can do is respond with the default error response.
|
|
542
542
|
|
|
543
543
|
Surprisingly enough, the last call related to error handling is `customizeGlobalConfig()`, which was already covered above. The reason why it's done this way is to allow you to tweak error handling when you work with `DefaultConfig`. The built-in error types are quite good, and many people may prefer to use them for a while before actually setting up their own error responses.
|
|
544
544
|
|
package/dist/OpenApi.d.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import 'zod-openapi/extend';
|
|
2
|
-
import { ErrorCode } from './enums/ErrorCode
|
|
3
|
-
import { AnyRoute } from './types/AnyRoute
|
|
4
|
-
import { RoutingFactory } from './services/RoutingFactory/RoutingFactory
|
|
5
|
-
import { RouteMap } from './types/RouteMap
|
|
6
|
-
import { SchemaGenerator } from './services/SchemaGenerator/SchemaGenerator
|
|
7
|
-
import { AnyConfig } from './types/config/AnyConfig
|
|
8
|
-
import { Logger } from './services/Logger/Logger
|
|
9
|
-
import { DescriptionChecker } from './services/DescriptionChecker/DescriptionChecker
|
|
10
|
-
import { DevelopmentUtils } from './services/DevelopmentUtils/DevelopmentUtils
|
|
11
|
-
import { ClientGenerator } from './services/ClientGenerator/ClientGenerator
|
|
12
|
-
import { ValidationUtils } from './services/ValidationUtils/ValidationUtils
|
|
13
|
-
import { Wrappers } from './types/Wrappers
|
|
14
|
-
import { Server } from './types/config/Server
|
|
15
|
-
import { RoutePath } from './types/RoutePath
|
|
16
|
-
import { SampleRouteType } from './enums/SampleRouteType
|
|
17
|
-
import { ConfigBuilder } from './services/ConfigBuilder/ConfigBuilder
|
|
18
|
-
import { DefaultConfig } from './services/ConfigBuilder/types/DefaultConfig
|
|
19
|
-
import { DefaultErrorMap } from './services/ConfigBuilder/types/DefaultErrorMap
|
|
20
|
-
import { DefaultRouteMap } from './services/ConfigBuilder/types/DefaultRouteMap
|
|
21
|
-
import { InitialBuilder } from './types/InitialBuilder
|
|
22
|
-
import { DefaultRouteContextMap } from './services/ConfigBuilder/types/DefaultRouteContextMap
|
|
23
|
-
import { DefaultRouteParamsMap } from './services/ConfigBuilder/types/DefaultRouteParamsMap
|
|
2
|
+
import { ErrorCode } from './enums/ErrorCode';
|
|
3
|
+
import { AnyRoute } from './types/AnyRoute';
|
|
4
|
+
import { RoutingFactory } from './services/RoutingFactory/RoutingFactory';
|
|
5
|
+
import { RouteMap } from './types/RouteMap';
|
|
6
|
+
import { SchemaGenerator } from './services/SchemaGenerator/SchemaGenerator';
|
|
7
|
+
import { AnyConfig } from './types/config/AnyConfig';
|
|
8
|
+
import { Logger } from './services/Logger/Logger';
|
|
9
|
+
import { DescriptionChecker } from './services/DescriptionChecker/DescriptionChecker';
|
|
10
|
+
import { DevelopmentUtils } from './services/DevelopmentUtils/DevelopmentUtils';
|
|
11
|
+
import { ClientGenerator } from './services/ClientGenerator/ClientGenerator';
|
|
12
|
+
import { ValidationUtils } from './services/ValidationUtils/ValidationUtils';
|
|
13
|
+
import { Wrappers } from './types/Wrappers';
|
|
14
|
+
import { Server } from './types/config/Server';
|
|
15
|
+
import { RoutePath } from './types/RoutePath';
|
|
16
|
+
import { SampleRouteType } from './enums/SampleRouteType';
|
|
17
|
+
import { ConfigBuilder } from './services/ConfigBuilder/ConfigBuilder';
|
|
18
|
+
import { DefaultConfig } from './services/ConfigBuilder/types/DefaultConfig';
|
|
19
|
+
import { DefaultErrorMap } from './services/ConfigBuilder/types/DefaultErrorMap';
|
|
20
|
+
import { DefaultRouteMap } from './services/ConfigBuilder/types/DefaultRouteMap';
|
|
21
|
+
import { InitialBuilder } from './types/InitialBuilder';
|
|
22
|
+
import { DefaultRouteContextMap } from './services/ConfigBuilder/types/DefaultRouteContextMap';
|
|
23
|
+
import { DefaultRouteParamsMap } from './services/ConfigBuilder/types/DefaultRouteParamsMap';
|
|
24
24
|
import z from 'zod';
|
|
25
25
|
export declare class OpenApi<TRouteTypes extends string, TErrorCodes extends string, TConfig extends AnyConfig<TRouteTypes, TErrorCodes>> {
|
|
26
26
|
static readonly builder: InitialBuilder;
|
|
@@ -52,7 +52,7 @@ export declare class OpenApi<TRouteTypes extends string, TErrorCodes extends str
|
|
|
52
52
|
}>;
|
|
53
53
|
protected handleError(e: unknown): {
|
|
54
54
|
status: number;
|
|
55
|
-
body: z.TypeOf<import("
|
|
55
|
+
body: z.TypeOf<import(".").OpenApiErrorConfigMap<TErrorCodes>[TErrorCodes]["responseValidator"]>;
|
|
56
56
|
};
|
|
57
57
|
protected static getBuilder(): ConfigBuilder<SampleRouteType, ErrorCode, DefaultErrorMap, DefaultRouteParamsMap, DefaultRouteContextMap, DefaultRouteMap, DefaultConfig>;
|
|
58
58
|
}
|
package/dist/OpenApi.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import 'zod-openapi/extend';
|
|
2
|
-
import { ErrorCode } from './enums/ErrorCode
|
|
3
|
-
import { ValidationLocation } from './enums/ValidationLocations
|
|
4
|
-
import { ValidationError } from './types/errors/ValidationError
|
|
5
|
-
import { RoutingFactory } from './services/RoutingFactory/RoutingFactory
|
|
6
|
-
import { SchemaGenerator } from './services/SchemaGenerator/SchemaGenerator
|
|
7
|
-
import { Method } from './enums/Methods
|
|
8
|
-
import { BuiltInError } from './types/errors/BuiltInError
|
|
9
|
-
import { Logger } from './services/Logger/Logger
|
|
10
|
-
import { DescriptionChecker } from './services/DescriptionChecker/DescriptionChecker
|
|
11
|
-
import { DevelopmentUtils } from './services/DevelopmentUtils/DevelopmentUtils
|
|
12
|
-
import { ClientGenerator } from './services/ClientGenerator/ClientGenerator
|
|
13
|
-
import { ValidationUtils } from './services/ValidationUtils/ValidationUtils
|
|
14
|
-
import { TanstackStartWrapper } from './services/TanstackStartWrapper/TanstackStartWrapper
|
|
15
|
-
import { ExpressWrapper } from './services/ExpressWrapper/ExpressWrapper
|
|
16
|
-
import { ConfigBuilder } from './services/ConfigBuilder/ConfigBuilder
|
|
2
|
+
import { ErrorCode } from './enums/ErrorCode';
|
|
3
|
+
import { ValidationLocation } from './enums/ValidationLocations';
|
|
4
|
+
import { ValidationError } from './types/errors/ValidationError';
|
|
5
|
+
import { RoutingFactory } from './services/RoutingFactory/RoutingFactory';
|
|
6
|
+
import { SchemaGenerator } from './services/SchemaGenerator/SchemaGenerator';
|
|
7
|
+
import { Method } from './enums/Methods';
|
|
8
|
+
import { BuiltInError } from './types/errors/BuiltInError';
|
|
9
|
+
import { Logger } from './services/Logger/Logger';
|
|
10
|
+
import { DescriptionChecker } from './services/DescriptionChecker/DescriptionChecker';
|
|
11
|
+
import { DevelopmentUtils } from './services/DevelopmentUtils/DevelopmentUtils';
|
|
12
|
+
import { ClientGenerator } from './services/ClientGenerator/ClientGenerator';
|
|
13
|
+
import { ValidationUtils } from './services/ValidationUtils/ValidationUtils';
|
|
14
|
+
import { TanstackStartWrapper } from './services/TanstackStartWrapper/TanstackStartWrapper';
|
|
15
|
+
import { ExpressWrapper } from './services/ExpressWrapper/ExpressWrapper';
|
|
16
|
+
import { ConfigBuilder } from './services/ConfigBuilder/ConfigBuilder';
|
|
17
17
|
import z from 'zod';
|
|
18
18
|
export class OpenApi {
|
|
19
19
|
static builder = OpenApi.getBuilder();
|
|
@@ -31,7 +31,7 @@ export class OpenApi {
|
|
|
31
31
|
servers = [];
|
|
32
32
|
constructor(config) {
|
|
33
33
|
this.config = config;
|
|
34
|
-
this.logger = new Logger('OpenAPI');
|
|
34
|
+
this.logger = config.logger ?? new Logger('OpenAPI');
|
|
35
35
|
if (config.logLevel) {
|
|
36
36
|
Logger.logLevel = config.logLevel;
|
|
37
37
|
}
|
|
@@ -45,7 +45,7 @@ export class OpenApi {
|
|
|
45
45
|
title: config.apiName ?? 'My API',
|
|
46
46
|
version: '3.1.0',
|
|
47
47
|
};
|
|
48
|
-
this.schemaGenerator = new SchemaGenerator(this.logger.
|
|
48
|
+
this.schemaGenerator = new SchemaGenerator(this.logger.derrive('SchemaGenerator'), info, this.config, this.routes, this.servers);
|
|
49
49
|
this.wrappers = {
|
|
50
50
|
tanstackStart: new TanstackStartWrapper(this),
|
|
51
51
|
express: new ExpressWrapper(this),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { TestUtils } from './services/TestUtils/TestUtils';
|
|
3
|
+
import { Method } from './enums/Methods';
|
|
4
|
+
import z from 'zod';
|
|
5
|
+
import { OpenApi } from './OpenApi';
|
|
6
|
+
import { SampleRouteType } from './enums/SampleRouteType';
|
|
7
|
+
import { ErrorCode } from './enums/ErrorCode';
|
|
8
|
+
import { ValidationLocation } from './enums/ValidationLocations';
|
|
9
|
+
import { LogLevel } from './services/Logger/types/LogLevel';
|
|
10
|
+
import { DefaultConfig } from './services/ConfigBuilder/types/DefaultConfig';
|
|
11
|
+
import { Logger } from './services/Logger/Logger';
|
|
12
|
+
describe('OpenApi', () => {
|
|
13
|
+
test('Happy Path', async () => {
|
|
14
|
+
const api = OpenApi.builder.create();
|
|
15
|
+
const route = api.factory.createRoute({
|
|
16
|
+
type: SampleRouteType.Public,
|
|
17
|
+
method: Method.GET,
|
|
18
|
+
path: '/test',
|
|
19
|
+
description: 'My Test Route',
|
|
20
|
+
validators: {
|
|
21
|
+
response: z.string().openapi({ description: 'Test Response' }),
|
|
22
|
+
},
|
|
23
|
+
handler: async () => {
|
|
24
|
+
return '1';
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
api.addRoutes('/', [route]);
|
|
28
|
+
const req = new Request('http://localhost/api/test', {});
|
|
29
|
+
const response = await api.processRootRoute(req);
|
|
30
|
+
expect(response.status).toBe(200);
|
|
31
|
+
expect(response.body).toBe('1');
|
|
32
|
+
});
|
|
33
|
+
describe('Logging', () => {
|
|
34
|
+
const consoleLogBackup = console.log;
|
|
35
|
+
let messages = [];
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
messages = [];
|
|
38
|
+
console.log = (...args) => {
|
|
39
|
+
messages.push(...args);
|
|
40
|
+
consoleLogBackup(...args);
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
console.log = consoleLogBackup;
|
|
45
|
+
});
|
|
46
|
+
test('Log level can be controlled', async () => {
|
|
47
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
48
|
+
basePath: '/api',
|
|
49
|
+
skipDescriptionsCheck: true,
|
|
50
|
+
}).create();
|
|
51
|
+
const route = api.factory.createRoute({
|
|
52
|
+
method: Method.GET,
|
|
53
|
+
type: SampleRouteType.Public,
|
|
54
|
+
path: '/',
|
|
55
|
+
description: 'My test route',
|
|
56
|
+
validators: {
|
|
57
|
+
response: z.number(),
|
|
58
|
+
},
|
|
59
|
+
handler: async () => 1,
|
|
60
|
+
});
|
|
61
|
+
api.addRoute(route);
|
|
62
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
expect(messages[0]).toContain('Calling route /');
|
|
65
|
+
});
|
|
66
|
+
test('Logs can be suppressed', async () => {
|
|
67
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
68
|
+
basePath: '/api',
|
|
69
|
+
skipDescriptionsCheck: true,
|
|
70
|
+
logLevel: LogLevel.error,
|
|
71
|
+
}).create();
|
|
72
|
+
const route = api.factory.createRoute({
|
|
73
|
+
method: Method.GET,
|
|
74
|
+
type: SampleRouteType.Public,
|
|
75
|
+
path: '/',
|
|
76
|
+
description: 'My test route',
|
|
77
|
+
validators: {
|
|
78
|
+
response: z.number(),
|
|
79
|
+
},
|
|
80
|
+
handler: async () => 1,
|
|
81
|
+
});
|
|
82
|
+
api.addRoute(route);
|
|
83
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
84
|
+
expect(response.status).toBe(200);
|
|
85
|
+
expect(messages.length, 'No logs should be shown').toBe(0);
|
|
86
|
+
});
|
|
87
|
+
test('Logger can be overriden', async () => {
|
|
88
|
+
class MooLogger extends Logger {
|
|
89
|
+
log() {
|
|
90
|
+
console.log('moo');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
94
|
+
basePath: '/api',
|
|
95
|
+
skipDescriptionsCheck: true,
|
|
96
|
+
logger: new MooLogger('Moo'),
|
|
97
|
+
logLevel: LogLevel.all,
|
|
98
|
+
}).create();
|
|
99
|
+
const route = api.factory.createRoute({
|
|
100
|
+
method: Method.GET,
|
|
101
|
+
type: SampleRouteType.Public,
|
|
102
|
+
path: '/',
|
|
103
|
+
description: 'My test route',
|
|
104
|
+
validators: {
|
|
105
|
+
response: z.number(),
|
|
106
|
+
},
|
|
107
|
+
handler: async () => 1,
|
|
108
|
+
});
|
|
109
|
+
api.addRoute(route);
|
|
110
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
111
|
+
expect(response.status).toBe(200);
|
|
112
|
+
expect(messages[0], 'Logs should be changed to "moo" lines').toBe('moo');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
test('Has 1 default server and it should be localhost', async () => {
|
|
116
|
+
const api = OpenApi.builder.create();
|
|
117
|
+
const servers = api.getServers();
|
|
118
|
+
expect(servers[0]?.url, 'Default server URL is incorrect').toBe('/api');
|
|
119
|
+
expect(servers[0]?.description, 'Default server description is incorrect').toBe('Local');
|
|
120
|
+
});
|
|
121
|
+
test('Can add more servers', async () => {
|
|
122
|
+
const api = OpenApi.builder.create();
|
|
123
|
+
api.addServer('http://production-website.com/api', 'Production');
|
|
124
|
+
const servers = api.getServers();
|
|
125
|
+
expect(servers[1]?.url, 'URL is incorrect').toBe('http://production-website.com/api');
|
|
126
|
+
expect(servers[1]?.description, 'Description is incorrect').toBe('Production');
|
|
127
|
+
});
|
|
128
|
+
test('Routes can be added via routemap', async () => {
|
|
129
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
130
|
+
basePath: '/api',
|
|
131
|
+
skipDescriptionsCheck: true,
|
|
132
|
+
}).create();
|
|
133
|
+
const route1 = api.factory.createRoute({
|
|
134
|
+
method: Method.GET,
|
|
135
|
+
type: SampleRouteType.Public,
|
|
136
|
+
path: '/one',
|
|
137
|
+
description: 'My test route',
|
|
138
|
+
validators: {
|
|
139
|
+
response: z.number(),
|
|
140
|
+
},
|
|
141
|
+
handler: async () => 1,
|
|
142
|
+
});
|
|
143
|
+
const route2 = api.factory.createRoute({
|
|
144
|
+
method: Method.GET,
|
|
145
|
+
type: SampleRouteType.Public,
|
|
146
|
+
path: '/two',
|
|
147
|
+
description: 'My test route',
|
|
148
|
+
validators: {
|
|
149
|
+
response: z.number(),
|
|
150
|
+
},
|
|
151
|
+
handler: async () => 2,
|
|
152
|
+
});
|
|
153
|
+
const route3 = api.factory.createRoute({
|
|
154
|
+
method: Method.GET,
|
|
155
|
+
type: SampleRouteType.Public,
|
|
156
|
+
path: '/three',
|
|
157
|
+
description: 'My test route',
|
|
158
|
+
validators: {
|
|
159
|
+
response: z.number(),
|
|
160
|
+
},
|
|
161
|
+
handler: async () => 3,
|
|
162
|
+
});
|
|
163
|
+
const routeMap = {
|
|
164
|
+
'/path1': [
|
|
165
|
+
route1,
|
|
166
|
+
route3,
|
|
167
|
+
],
|
|
168
|
+
'/path2': [
|
|
169
|
+
route2,
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
api.addRouteMap(routeMap);
|
|
173
|
+
const response1 = await TestUtils.sendRequest(api, '/path1/one', Method.GET);
|
|
174
|
+
expect(response1.body).toBe(1);
|
|
175
|
+
const response2error = await TestUtils.sendRequest(api, '/path1/two', Method.GET);
|
|
176
|
+
expect(response2error.status).toBe(404);
|
|
177
|
+
const response2 = await TestUtils.sendRequest(api, '/path2/two', Method.GET);
|
|
178
|
+
expect(response2.body).toBe(2);
|
|
179
|
+
const response3 = await TestUtils.sendRequest(api, '/path1/three', Method.GET);
|
|
180
|
+
expect(response3.body).toBe(3);
|
|
181
|
+
});
|
|
182
|
+
describe('Context and Route Props Constructor', () => {
|
|
183
|
+
let RouteType;
|
|
184
|
+
(function (RouteType) {
|
|
185
|
+
RouteType["User"] = "User";
|
|
186
|
+
})(RouteType || (RouteType = {}));
|
|
187
|
+
test('Context is working', async () => {
|
|
188
|
+
const api = OpenApi.builder.customizeRoutes(RouteType).defineRouteContexts({
|
|
189
|
+
[RouteType.User]: async () => {
|
|
190
|
+
return { currentPermission: 'user' };
|
|
191
|
+
},
|
|
192
|
+
}).defineRoutes({
|
|
193
|
+
[RouteType.User]: {
|
|
194
|
+
authorization: false,
|
|
195
|
+
},
|
|
196
|
+
}).create();
|
|
197
|
+
const route = api.factory.createRoute({
|
|
198
|
+
type: RouteType.User,
|
|
199
|
+
method: Method.GET,
|
|
200
|
+
path: '/',
|
|
201
|
+
description: 'My fantastic route',
|
|
202
|
+
validators: {
|
|
203
|
+
response: z.string().openapi({ description: 'response' }),
|
|
204
|
+
},
|
|
205
|
+
handler: async (context) => {
|
|
206
|
+
return context.currentPermission;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
api.addRoutes('/', [route]);
|
|
210
|
+
const req = TestUtils.createRequest('/api', Method.GET);
|
|
211
|
+
const res = await api.processRootRoute(req);
|
|
212
|
+
expect(res.status).toBe(200);
|
|
213
|
+
expect(res.body).toBe('user');
|
|
214
|
+
});
|
|
215
|
+
test('Route props working', async () => {
|
|
216
|
+
const api = OpenApi.builder.customizeRoutes(RouteType).defineRouteExtraProps({
|
|
217
|
+
[RouteType.User]: z.object({
|
|
218
|
+
permission: z.enum(['read', 'write']),
|
|
219
|
+
}),
|
|
220
|
+
}).defineRouteContexts({
|
|
221
|
+
[RouteType.User]: async (ctx) => {
|
|
222
|
+
return {
|
|
223
|
+
routePermission: ctx.route.permission,
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
}).defineRoutes({
|
|
227
|
+
[RouteType.User]: {
|
|
228
|
+
authorization: false,
|
|
229
|
+
},
|
|
230
|
+
}).create();
|
|
231
|
+
const route = api.factory.createRoute({
|
|
232
|
+
type: RouteType.User,
|
|
233
|
+
method: Method.GET,
|
|
234
|
+
path: '/',
|
|
235
|
+
description: 'Something long',
|
|
236
|
+
validators: {
|
|
237
|
+
response: z.string().openapi({ description: 'Hello threre' }),
|
|
238
|
+
},
|
|
239
|
+
handler: async (context) => context.routePermission,
|
|
240
|
+
permission: 'read',
|
|
241
|
+
});
|
|
242
|
+
api.addRoutes('/', [route]);
|
|
243
|
+
const req = TestUtils.createRequest('/api', Method.GET);
|
|
244
|
+
const res = await api.processRootRoute(req);
|
|
245
|
+
expect(res.status).toBe(200);
|
|
246
|
+
expect(res.body).toBe('read');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('Route params', async () => {
|
|
250
|
+
test('Can process arrays in query', async () => {
|
|
251
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
252
|
+
basePath: '/api',
|
|
253
|
+
skipDescriptionsCheck: true,
|
|
254
|
+
}).create();
|
|
255
|
+
const route = api.factory.createRoute({
|
|
256
|
+
method: Method.GET,
|
|
257
|
+
type: SampleRouteType.Public,
|
|
258
|
+
path: '/',
|
|
259
|
+
description: 'My test route',
|
|
260
|
+
validators: {
|
|
261
|
+
query: z.object({
|
|
262
|
+
ids: api.validators.strings.number.array(),
|
|
263
|
+
}),
|
|
264
|
+
response: z.number().array(),
|
|
265
|
+
},
|
|
266
|
+
handler: async (ctx) => ctx.params.query.ids,
|
|
267
|
+
});
|
|
268
|
+
api.addRoute(route);
|
|
269
|
+
const response = await TestUtils.sendRequest(api, '/api/?ids=1&ids=2&ids=3', Method.GET);
|
|
270
|
+
expect(response.status).toBe(200);
|
|
271
|
+
expect(response.body).toEqual([1, 2, 3]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('Validation and Errors', () => {
|
|
275
|
+
test('Validates path', async () => {
|
|
276
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
277
|
+
basePath: '/api',
|
|
278
|
+
skipDescriptionsCheck: true,
|
|
279
|
+
}).create();
|
|
280
|
+
const route = api.factory.createRoute({
|
|
281
|
+
method: Method.GET,
|
|
282
|
+
type: SampleRouteType.Public,
|
|
283
|
+
path: '/{id}',
|
|
284
|
+
description: 'My test route',
|
|
285
|
+
validators: {
|
|
286
|
+
path: z.object({
|
|
287
|
+
id: api.validators.strings.number,
|
|
288
|
+
}),
|
|
289
|
+
response: z.object({ ok: z.boolean() }),
|
|
290
|
+
},
|
|
291
|
+
handler: async () => ({ ok: true }),
|
|
292
|
+
});
|
|
293
|
+
api.addRoute(route);
|
|
294
|
+
const response = await TestUtils.sendRequest(api, '/api/23', Method.GET);
|
|
295
|
+
expect(response.body.ok).toBe(true);
|
|
296
|
+
const response2 = await TestUtils.sendRequest(api, '/api/check', Method.GET);
|
|
297
|
+
expect(response2.status).toBe(400);
|
|
298
|
+
expect(response2.body?.error?.code).toBe(ErrorCode.ValidationFailed);
|
|
299
|
+
expect(response2.body?.error?.location).toBe(ValidationLocation.Path);
|
|
300
|
+
});
|
|
301
|
+
test('Validates query', async () => {
|
|
302
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
303
|
+
basePath: '/api',
|
|
304
|
+
skipDescriptionsCheck: true,
|
|
305
|
+
}).create();
|
|
306
|
+
const route = api.factory.createRoute({
|
|
307
|
+
method: Method.GET,
|
|
308
|
+
type: SampleRouteType.Public,
|
|
309
|
+
path: '/',
|
|
310
|
+
description: 'My test route',
|
|
311
|
+
validators: {
|
|
312
|
+
query: z.object({
|
|
313
|
+
id: api.validators.strings.number,
|
|
314
|
+
}),
|
|
315
|
+
response: z.object({ ok: z.boolean() }),
|
|
316
|
+
},
|
|
317
|
+
handler: async () => ({ ok: true }),
|
|
318
|
+
});
|
|
319
|
+
api.addRoute(route);
|
|
320
|
+
const response = await TestUtils.sendRequest(api, '/api/?id=12', Method.GET);
|
|
321
|
+
expect(response.body.ok).toBe(true);
|
|
322
|
+
const response2 = await TestUtils.sendRequest(api, '/api/?id=check', Method.GET);
|
|
323
|
+
expect(response2.status).toBe(400);
|
|
324
|
+
expect(response2.body?.error?.code).toBe(ErrorCode.ValidationFailed);
|
|
325
|
+
expect(response2.body?.error?.location).toBe(ValidationLocation.Query);
|
|
326
|
+
});
|
|
327
|
+
test('Validates response', async () => {
|
|
328
|
+
const api = OpenApi.builder.defineGlobalConfig({
|
|
329
|
+
basePath: '/api',
|
|
330
|
+
skipDescriptionsCheck: true,
|
|
331
|
+
}).create();
|
|
332
|
+
const route = api.factory.createRoute({
|
|
333
|
+
method: Method.GET,
|
|
334
|
+
type: SampleRouteType.Public,
|
|
335
|
+
path: '/',
|
|
336
|
+
description: 'My test route',
|
|
337
|
+
validators: {
|
|
338
|
+
response: z.object({ ok: z.boolean() }),
|
|
339
|
+
},
|
|
340
|
+
handler: async () => ({ ok: 1 }),
|
|
341
|
+
});
|
|
342
|
+
api.addRoute(route);
|
|
343
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
344
|
+
expect(response.status, 'Response errors should come with status 500').toBe(500);
|
|
345
|
+
expect(response.body.error, 'Response validation errors come with code UnknownError').toBe(ErrorCode.UnknownError);
|
|
346
|
+
});
|
|
347
|
+
test('Responds with default error if no error handler present', async () => {
|
|
348
|
+
const conf = new DefaultConfig();
|
|
349
|
+
conf.handleError = undefined;
|
|
350
|
+
conf.skipDescriptionsCheck = true;
|
|
351
|
+
const api = OpenApi.builder.create(SampleRouteType, ErrorCode, conf);
|
|
352
|
+
const route = api.factory.createRoute({
|
|
353
|
+
method: Method.GET,
|
|
354
|
+
type: SampleRouteType.Public,
|
|
355
|
+
path: '/',
|
|
356
|
+
description: 'My test route',
|
|
357
|
+
validators: {
|
|
358
|
+
response: z.object({ ok: z.boolean() }),
|
|
359
|
+
},
|
|
360
|
+
handler: async () => {
|
|
361
|
+
throw new Error('Test');
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
api.addRoute(route);
|
|
365
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
366
|
+
expect(response.status, 'Default default error has status 500').toBe(500);
|
|
367
|
+
expect(response.body.error, 'Default default error is UnknownError').toBe(ErrorCode.UnknownError);
|
|
368
|
+
});
|
|
369
|
+
test('Responds with default error if there is error in error handler', async () => {
|
|
370
|
+
const conf = new DefaultConfig();
|
|
371
|
+
conf.handleError = () => {
|
|
372
|
+
throw new Error('Something went wrong');
|
|
373
|
+
};
|
|
374
|
+
conf.skipDescriptionsCheck = true;
|
|
375
|
+
const api = OpenApi.builder.create(SampleRouteType, ErrorCode, conf);
|
|
376
|
+
const route = api.factory.createRoute({
|
|
377
|
+
method: Method.GET,
|
|
378
|
+
type: SampleRouteType.Public,
|
|
379
|
+
path: '/',
|
|
380
|
+
description: 'My test route',
|
|
381
|
+
validators: {
|
|
382
|
+
response: z.object({ ok: z.boolean() }),
|
|
383
|
+
},
|
|
384
|
+
handler: async () => {
|
|
385
|
+
throw new Error('Test');
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
api.addRoute(route);
|
|
389
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
390
|
+
expect(response.status, 'Default default error has status 500').toBe(500);
|
|
391
|
+
expect(response.body.error, 'Default default error is UnknownError').toBe(ErrorCode.UnknownError);
|
|
392
|
+
});
|
|
393
|
+
test('Responds with default error if error handler responded with unregistered error', async () => {
|
|
394
|
+
const conf = new DefaultConfig();
|
|
395
|
+
conf.handleError = () => {
|
|
396
|
+
return { code: ErrorCode.UnknownError, body: 'Something' };
|
|
397
|
+
};
|
|
398
|
+
conf.skipDescriptionsCheck = true;
|
|
399
|
+
const api = OpenApi.builder.create(SampleRouteType, ErrorCode, conf);
|
|
400
|
+
const route = api.factory.createRoute({
|
|
401
|
+
method: Method.GET,
|
|
402
|
+
type: SampleRouteType.Public,
|
|
403
|
+
path: '/',
|
|
404
|
+
description: 'My test route',
|
|
405
|
+
validators: {
|
|
406
|
+
response: z.object({ ok: z.boolean() }),
|
|
407
|
+
},
|
|
408
|
+
handler: async () => {
|
|
409
|
+
throw new Error('Test');
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
api.addRoute(route);
|
|
413
|
+
const response = await TestUtils.sendRequest(api, '/api', Method.GET);
|
|
414
|
+
expect(response.status, 'Default default error has status 500').toBe(500);
|
|
415
|
+
expect(response.body.error, 'Default default error is UnknownError').toBe(ErrorCode.UnknownError);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|