@furystack/rest-service 12.3.5 → 14.0.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/CHANGELOG.md +55 -0
- package/README.md +47 -28
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/index.d.ts +0 -1
- package/esm/actions/index.d.ts.map +1 -1
- package/esm/actions/index.js +0 -1
- package/esm/actions/index.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.js +1 -1
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/password-login-action.js +1 -1
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.js +1 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -15
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -24
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +0 -2
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +53 -87
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +91 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +35 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +21 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +31 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +76 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +189 -337
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +6 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/openapi/generate-openapi-document.d.ts +0 -8
- package/esm/openapi/generate-openapi-document.d.ts.map +1 -1
- package/esm/openapi/generate-openapi-document.js +0 -2
- package/esm/openapi/generate-openapi-document.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +60 -82
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +57 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +26 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.integration.spec.js +26 -83
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +8 -8
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/index.ts +0 -1
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +1 -1
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +1 -1
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +1 -1
- package/src/authorize.spec.ts +37 -39
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -38
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/endpoint-generators/with-schema-and-openapi-action.ts +0 -3
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +1 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +51 -25
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +203 -383
- package/src/http-user-context.ts +114 -91
- package/src/index.ts +6 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +13 -12
- package/src/openapi/generate-openapi-document.ts +0 -3
- package/src/proxy-manager.spec.ts +152 -182
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +68 -87
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +51 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +68 -0
- package/src/validate.integration.spec.ts +39 -97
- package/esm/actions/login-action.spec.d.ts +0 -2
- package/esm/actions/login-action.spec.d.ts.map +0 -1
- package/esm/actions/login-action.spec.js +0 -34
- package/esm/actions/login-action.spec.js.map +0 -1
- package/esm/actions/login.d.ts +0 -19
- package/esm/actions/login.d.ts.map +0 -1
- package/esm/actions/login.js +0 -27
- package/esm/actions/login.js.map +0 -1
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -180
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.d.ts +0 -2
- package/esm/swagger/generate-swagger-json.spec.d.ts.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.js +0 -215
- package/esm/swagger/generate-swagger-json.spec.js.map +0 -1
- package/src/actions/login-action.spec.ts +0 -42
- package/src/actions/login.ts +0 -32
- package/src/api-manager.ts +0 -308
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
- package/src/swagger/generate-swagger-json.spec.ts +0 -250
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Injector } from '@furystack/inject'
|
|
2
|
-
import { usingAsync } from '@furystack/utils'
|
|
3
|
-
import type { IncomingMessage, ServerResponse } from 'http'
|
|
4
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
-
import { HttpUserContext } from '../http-user-context.js'
|
|
6
|
-
import { LoginAction } from './login.js'
|
|
7
|
-
|
|
8
|
-
describe('LoginAction', () => {
|
|
9
|
-
const request = { url: 'https://google.com' } as IncomingMessage
|
|
10
|
-
const response = {} as ServerResponse
|
|
11
|
-
|
|
12
|
-
it('Returns the provided user with 200 on success', async () => {
|
|
13
|
-
const testUser = { Name: 'Userke' }
|
|
14
|
-
await usingAsync(new Injector(), async (i) => {
|
|
15
|
-
i.setExplicitInstance(
|
|
16
|
-
{
|
|
17
|
-
authenticateUser: vi.fn(async () => testUser),
|
|
18
|
-
cookieLogin: vi.fn(async () => testUser),
|
|
19
|
-
authentication: {},
|
|
20
|
-
},
|
|
21
|
-
HttpUserContext,
|
|
22
|
-
)
|
|
23
|
-
const result = await LoginAction({
|
|
24
|
-
request,
|
|
25
|
-
response,
|
|
26
|
-
injector: i,
|
|
27
|
-
getBody: async () => ({ username: 'testuser', password: 'alma' }),
|
|
28
|
-
})
|
|
29
|
-
expect(result.chunk).toEqual(testUser)
|
|
30
|
-
expect(result.statusCode).toBe(200)
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('Returns throw error with 400 on fail', async () => {
|
|
35
|
-
await usingAsync(new Injector(), async (i) => {
|
|
36
|
-
i.setExplicitInstance({ cookieLogin: async () => Promise.reject(new Error(':(')) }, HttpUserContext)
|
|
37
|
-
await expect(
|
|
38
|
-
LoginAction({ request, response, injector: i, getBody: async () => ({ username: '', password: '' }) }),
|
|
39
|
-
).rejects.toThrowError('Login Failed')
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
})
|
package/src/actions/login.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { User } from '@furystack/core'
|
|
2
|
-
import { RequestError } from '@furystack/rest'
|
|
3
|
-
import { sleepAsync } from '@furystack/utils'
|
|
4
|
-
|
|
5
|
-
import { HttpUserContext } from '../http-user-context.js'
|
|
6
|
-
import type { RequestAction } from '../request-action-implementation.js'
|
|
7
|
-
import { JsonResult } from '../request-action-implementation.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Action that logs in the current user.
|
|
11
|
-
* Should be called with a JSON POST body with `username` and `password` fields.
|
|
12
|
-
* Returns the current user instance.
|
|
13
|
-
*
|
|
14
|
-
* @deprecated Use `createPasswordLoginAction(createCookieLoginStrategy(injector))` instead.
|
|
15
|
-
* This static action resolves services from the request-scoped injector on
|
|
16
|
-
* every call; the factory approach captures them once at setup time.
|
|
17
|
-
*/
|
|
18
|
-
export const LoginAction: RequestAction<{
|
|
19
|
-
result: User
|
|
20
|
-
body: { username: string; password: string }
|
|
21
|
-
}> = async ({ injector, getBody, response }) => {
|
|
22
|
-
const userContext = injector.getInstance(HttpUserContext)
|
|
23
|
-
const body = await getBody()
|
|
24
|
-
try {
|
|
25
|
-
const user = await userContext.authenticateUser(body.username, body.password)
|
|
26
|
-
await userContext.cookieLogin(user, response)
|
|
27
|
-
return JsonResult(user, 200)
|
|
28
|
-
} catch {
|
|
29
|
-
await sleepAsync(Math.random() * 1000)
|
|
30
|
-
throw new RequestError('Login Failed', 400)
|
|
31
|
-
}
|
|
32
|
-
}
|
package/src/api-manager.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import type { User } from '@furystack/core'
|
|
2
|
-
import { IdentityContext } from '@furystack/core'
|
|
3
|
-
import type { Injector } from '@furystack/inject'
|
|
4
|
-
import { Injectable, Injected } from '@furystack/inject'
|
|
5
|
-
import type { Method, RestApi } from '@furystack/rest'
|
|
6
|
-
import { RequestError, deserializeQueryString } from '@furystack/rest'
|
|
7
|
-
import { PathHelper, usingAsync } from '@furystack/utils'
|
|
8
|
-
import type { MatchFunction } from 'path-to-regexp'
|
|
9
|
-
import { match } from 'path-to-regexp'
|
|
10
|
-
import { ErrorAction } from './actions/error-action.js'
|
|
11
|
-
import { NotFoundAction } from './actions/not-found-action.js'
|
|
12
|
-
import { addCorsHeaders } from './add-cors-header.js'
|
|
13
|
-
import { CreateGetSchemaAction } from './endpoint-generators/create-get-schema-action.js'
|
|
14
|
-
import {
|
|
15
|
-
CreateGetOpenApiDocumentAction,
|
|
16
|
-
CreateDeprecatedSwaggerRedirect,
|
|
17
|
-
} from './endpoint-generators/create-get-openapi-document-action.js'
|
|
18
|
-
import { HttpUserContext } from './http-user-context.js'
|
|
19
|
-
import type { CorsOptions } from './models/cors-options.js'
|
|
20
|
-
import { readPostBody } from './read-post-body.js'
|
|
21
|
-
import type { RequestAction } from './request-action-implementation.js'
|
|
22
|
-
import type { OnRequest, ServerApi } from './server-manager.js'
|
|
23
|
-
import { ServerManager } from './server-manager.js'
|
|
24
|
-
import './server-response-extensions.js'
|
|
25
|
-
|
|
26
|
-
export type RestApiImplementation<T extends RestApi> = {
|
|
27
|
-
[TMethod in keyof T]: {
|
|
28
|
-
[TUrl in keyof T[TMethod]]: T[TMethod][TUrl] extends { result: unknown } ? RequestAction<T[TMethod][TUrl]> : never
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ImplementApiOptions<T extends RestApi> {
|
|
33
|
-
/**
|
|
34
|
-
* The structure of the implemented API.
|
|
35
|
-
*/
|
|
36
|
-
api: RestApiImplementation<T>
|
|
37
|
-
/**
|
|
38
|
-
* The Injector instance to use for dependency injection in the API actions.
|
|
39
|
-
*/
|
|
40
|
-
injector: Injector
|
|
41
|
-
/**
|
|
42
|
-
* The host name for the API Server. If not provided, the default host (ServerManager.DEFAULT_HOST) will be used.
|
|
43
|
-
*/
|
|
44
|
-
hostName?: string
|
|
45
|
-
/**
|
|
46
|
-
* The root path for the API. This will be prepended to all API paths.
|
|
47
|
-
*/
|
|
48
|
-
root: string
|
|
49
|
-
/**
|
|
50
|
-
* The port on which the API server will listen.
|
|
51
|
-
*/
|
|
52
|
-
port: number
|
|
53
|
-
/**
|
|
54
|
-
* CORS options to configure Cross-Origin Resource Sharing for the API.
|
|
55
|
-
*/
|
|
56
|
-
cors?: CorsOptions
|
|
57
|
-
/**
|
|
58
|
-
* An optional function to deserialize query parameters from the URL.
|
|
59
|
-
* This function should take a query string (e.g., "?key=value") and return an object with the parsed parameters.
|
|
60
|
-
* If not provided, the default deserialization will be used.
|
|
61
|
-
*/
|
|
62
|
-
deserializeQueryParams?: (param: string) => any
|
|
63
|
-
/**
|
|
64
|
-
* Adds an additional 'GET /schema' endpoint that returns the schema definitions of the API.
|
|
65
|
-
* Also adds a 'GET /openapi.json' endpoint that returns the API schema in OpenAPI 3.1 format.
|
|
66
|
-
*/
|
|
67
|
-
enableGetSchema?: boolean
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Optional name for the API, used in the generated schema.
|
|
71
|
-
* This can be useful for documentation or identification purposes.
|
|
72
|
-
*/
|
|
73
|
-
name?: string
|
|
74
|
-
/**
|
|
75
|
-
* Optional description for the API, used in the generated schema.
|
|
76
|
-
* This can provide additional context or information about the API's purpose.
|
|
77
|
-
*/
|
|
78
|
-
description?: string
|
|
79
|
-
/**
|
|
80
|
-
* Optional version for the API, used in the generated schema.
|
|
81
|
-
* This can help in versioning the API and tracking changes over time.
|
|
82
|
-
*/
|
|
83
|
-
version?: string
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type NewCompiledApiEntry = {
|
|
87
|
-
method: Method
|
|
88
|
-
fullPath: string
|
|
89
|
-
matcher: MatchFunction<Partial<Record<string, string | string[]>>>
|
|
90
|
-
action: RequestAction<any>
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export type NewCompiledApi = {
|
|
94
|
-
[K in Method]?: NewCompiledApiEntry[]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export type OnRequestOptions = OnRequest & {
|
|
98
|
-
compiledApi: NewCompiledApi
|
|
99
|
-
hostName?: string
|
|
100
|
-
port: number
|
|
101
|
-
rootApiPath: string
|
|
102
|
-
injector: Injector
|
|
103
|
-
cors?: CorsOptions
|
|
104
|
-
supportedMethods: string[]
|
|
105
|
-
deserializeQueryParams?: (param: string) => Record<string, unknown>
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@Injectable({ lifetime: 'singleton' })
|
|
109
|
-
export class ApiManager {
|
|
110
|
-
private getSuportedMethods(api: RestApiImplementation<RestApi>): Method[] {
|
|
111
|
-
return Object.keys(api) as Method[]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private compileApi<T extends RestApi>(api: RestApiImplementation<T>, root: string) {
|
|
115
|
-
const supportedMethods = this.getSuportedMethods(api)
|
|
116
|
-
|
|
117
|
-
const compiledApi: NewCompiledApi = {}
|
|
118
|
-
supportedMethods.forEach((method: Method) => {
|
|
119
|
-
compiledApi[method] = [
|
|
120
|
-
...Object.entries(api[method as keyof typeof api]).map(([path, action]) => {
|
|
121
|
-
const fullPath = `/${PathHelper.normalize(PathHelper.joinPaths(root, path))}`
|
|
122
|
-
const matcher = match(fullPath, { decode: decodeURIComponent })
|
|
123
|
-
return { method, fullPath, matcher, action: action as RequestAction<any> }
|
|
124
|
-
}),
|
|
125
|
-
]
|
|
126
|
-
})
|
|
127
|
-
return compiledApi
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
public async addApi<T extends RestApi>({
|
|
131
|
-
api,
|
|
132
|
-
hostName,
|
|
133
|
-
port,
|
|
134
|
-
root,
|
|
135
|
-
cors,
|
|
136
|
-
injector,
|
|
137
|
-
deserializeQueryParams,
|
|
138
|
-
enableGetSchema,
|
|
139
|
-
name,
|
|
140
|
-
description,
|
|
141
|
-
version,
|
|
142
|
-
}: ImplementApiOptions<T>) {
|
|
143
|
-
const extendedApi: typeof api = enableGetSchema
|
|
144
|
-
? {
|
|
145
|
-
...api,
|
|
146
|
-
GET: {
|
|
147
|
-
...api.GET,
|
|
148
|
-
'/schema': CreateGetSchemaAction(api, name, description, version),
|
|
149
|
-
'/openapi.json': CreateGetOpenApiDocumentAction(api, name, description, version),
|
|
150
|
-
'/swagger.json': CreateDeprecatedSwaggerRedirect(api, name, description, version),
|
|
151
|
-
},
|
|
152
|
-
}
|
|
153
|
-
: api
|
|
154
|
-
|
|
155
|
-
const supportedMethods = this.getSuportedMethods(extendedApi)
|
|
156
|
-
const rootApiPath = PathHelper.normalize(root)
|
|
157
|
-
const server = await this.serverManager.getOrCreate({ hostName, port })
|
|
158
|
-
const compiledApi = this.compileApi(extendedApi, root)
|
|
159
|
-
const serverApi = {
|
|
160
|
-
shouldExec: (msg) =>
|
|
161
|
-
this.shouldExecRequest({
|
|
162
|
-
...msg,
|
|
163
|
-
method: msg.req.method?.toUpperCase() as Method,
|
|
164
|
-
rootApiPath,
|
|
165
|
-
supportedMethods,
|
|
166
|
-
url: PathHelper.normalize(msg.req.url || ''),
|
|
167
|
-
}),
|
|
168
|
-
onRequest: (msg) =>
|
|
169
|
-
this.onMessage({
|
|
170
|
-
...msg,
|
|
171
|
-
compiledApi,
|
|
172
|
-
rootApiPath,
|
|
173
|
-
port,
|
|
174
|
-
supportedMethods,
|
|
175
|
-
cors,
|
|
176
|
-
injector,
|
|
177
|
-
hostName,
|
|
178
|
-
deserializeQueryParams,
|
|
179
|
-
}),
|
|
180
|
-
} satisfies ServerApi
|
|
181
|
-
server.apis.push(serverApi)
|
|
182
|
-
return serverApi
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
public shouldExecRequest(options: {
|
|
186
|
-
method?: Method
|
|
187
|
-
url?: string
|
|
188
|
-
rootApiPath: string
|
|
189
|
-
supportedMethods: Method[]
|
|
190
|
-
}): boolean {
|
|
191
|
-
return options.method &&
|
|
192
|
-
options.url &&
|
|
193
|
-
(options.supportedMethods.includes(options.method) || options.method === 'OPTIONS') &&
|
|
194
|
-
PathHelper.matchesBaseUrl(options.url, options.rootApiPath)
|
|
195
|
-
? true
|
|
196
|
-
: false
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private getActionFromEndpoint(compiledEndpoint: NewCompiledApi, fullUrl: URL, method: Method) {
|
|
200
|
-
let resolvedParams: unknown
|
|
201
|
-
const action = compiledEndpoint[method]?.find((route) => {
|
|
202
|
-
const result = route.matcher(fullUrl.pathname)
|
|
203
|
-
if (result) {
|
|
204
|
-
resolvedParams = result.params
|
|
205
|
-
}
|
|
206
|
-
return result
|
|
207
|
-
})
|
|
208
|
-
return (
|
|
209
|
-
action && {
|
|
210
|
-
...action,
|
|
211
|
-
params: resolvedParams,
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private async executeAction({
|
|
217
|
-
injector,
|
|
218
|
-
req,
|
|
219
|
-
res,
|
|
220
|
-
fullUrl,
|
|
221
|
-
action,
|
|
222
|
-
deserializeQueryParams,
|
|
223
|
-
params,
|
|
224
|
-
}: OnRequestOptions & {
|
|
225
|
-
fullUrl: URL
|
|
226
|
-
action: RequestAction<{ body: object; result: object; query: object; url: object; headers: object }>
|
|
227
|
-
params: unknown
|
|
228
|
-
}) {
|
|
229
|
-
await usingAsync(injector.createChild({ owner: action }), async (i) => {
|
|
230
|
-
const httpUserContext = i.getInstance(HttpUserContext)
|
|
231
|
-
i.setExplicitInstance<IdentityContext>(
|
|
232
|
-
{
|
|
233
|
-
getCurrentUser: <TUser extends User>() => httpUserContext.getCurrentUser(req) as Promise<TUser>,
|
|
234
|
-
isAuthorized: (...roles) => httpUserContext.isAuthorized(req, ...roles),
|
|
235
|
-
isAuthenticated: () => httpUserContext.isAuthenticated(req),
|
|
236
|
-
},
|
|
237
|
-
IdentityContext,
|
|
238
|
-
)
|
|
239
|
-
try {
|
|
240
|
-
const actionResult = await action({
|
|
241
|
-
request: req,
|
|
242
|
-
response: res,
|
|
243
|
-
injector: i,
|
|
244
|
-
getBody: () => readPostBody<any>(req),
|
|
245
|
-
headers: req.headers,
|
|
246
|
-
getQuery: () =>
|
|
247
|
-
deserializeQueryParams ? deserializeQueryParams(fullUrl.search) : deserializeQueryString(fullUrl.search),
|
|
248
|
-
getUrlParams: () => {
|
|
249
|
-
return params as object
|
|
250
|
-
},
|
|
251
|
-
})
|
|
252
|
-
res.sendActionResult(actionResult)
|
|
253
|
-
} catch (error) {
|
|
254
|
-
const errorActionResult = await ErrorAction({
|
|
255
|
-
request: req,
|
|
256
|
-
response: res,
|
|
257
|
-
injector: i,
|
|
258
|
-
getBody: async () => error,
|
|
259
|
-
})
|
|
260
|
-
res.sendActionResult(errorActionResult)
|
|
261
|
-
}
|
|
262
|
-
return
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private async onMessage(options: OnRequestOptions) {
|
|
267
|
-
const protocol = 'http://'
|
|
268
|
-
const host = `${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
|
|
269
|
-
const fullUrl = new URL(PathHelper.joinUrl(`${protocol}${host}`, options.req.url as string))
|
|
270
|
-
|
|
271
|
-
if (options.cors) {
|
|
272
|
-
addCorsHeaders(options.cors, options.req, options.res)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (options.req.method === 'OPTIONS') {
|
|
276
|
-
options.res.writeHead(200)
|
|
277
|
-
options.res.end()
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let action: ReturnType<typeof this.getActionFromEndpoint>
|
|
282
|
-
try {
|
|
283
|
-
action = this.getActionFromEndpoint(options.compiledApi, fullUrl, options.req.method?.toUpperCase() as Method)
|
|
284
|
-
} catch (error) {
|
|
285
|
-
const responseError =
|
|
286
|
-
error instanceof URIError ? new RequestError('Failed to decode URL path parameter', 400) : (error as Error)
|
|
287
|
-
const errorActionResult = await ErrorAction({
|
|
288
|
-
request: options.req,
|
|
289
|
-
response: options.res,
|
|
290
|
-
injector: options.injector,
|
|
291
|
-
getBody: async () => responseError,
|
|
292
|
-
})
|
|
293
|
-
options.res.sendActionResult(errorActionResult)
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (action) {
|
|
298
|
-
await this.executeAction({ ...options, ...action, fullUrl })
|
|
299
|
-
} else {
|
|
300
|
-
options.res.sendActionResult(
|
|
301
|
-
await NotFoundAction({ injector: options.injector, request: options.req, response: options.res }),
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
@Injected(ServerManager)
|
|
307
|
-
declare private readonly serverManager: ServerManager
|
|
308
|
-
}
|
package/src/proxy-manager.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { Injectable, Injected } from '@furystack/inject'
|
|
2
|
-
import { EventHub, PathHelper, type ListenerErrorPayload } from '@furystack/utils'
|
|
3
|
-
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
|
|
4
|
-
import type { Duplex } from 'stream'
|
|
5
|
-
import { HttpProxyHandler } from './http-proxy-handler.js'
|
|
6
|
-
import { PathProcessor } from './path-processor.js'
|
|
7
|
-
import { ServerManager } from './server-manager.js'
|
|
8
|
-
import { WebSocketProxyHandler } from './websocket-proxy-handler.js'
|
|
9
|
-
|
|
10
|
-
export interface ProxyOptions {
|
|
11
|
-
sourceBaseUrl: string
|
|
12
|
-
targetBaseUrl: string
|
|
13
|
-
pathRewrite?: (sourcePath: string) => string
|
|
14
|
-
sourceHostName?: string
|
|
15
|
-
sourcePort: number
|
|
16
|
-
headers?: (originalHeaders: OutgoingHttpHeaders) => OutgoingHttpHeaders
|
|
17
|
-
cookies?: (originalCookies: string[]) => string[]
|
|
18
|
-
responseCookies?: (responseCookies: string[]) => string[]
|
|
19
|
-
timeout?: number
|
|
20
|
-
enableWebsockets?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Manages HTTP and WebSocket proxy configurations and routing
|
|
25
|
-
*/
|
|
26
|
-
@Injectable({ lifetime: 'singleton' })
|
|
27
|
-
export class ProxyManager extends EventHub<{
|
|
28
|
-
onProxyFailed: { from: string; to: string; error: unknown }
|
|
29
|
-
onWebSocketProxyFailed: { from: string; to: string; error: unknown }
|
|
30
|
-
onListenerError: ListenerErrorPayload
|
|
31
|
-
}> {
|
|
32
|
-
@Injected(ServerManager)
|
|
33
|
-
declare private readonly serverManager: ServerManager
|
|
34
|
-
|
|
35
|
-
private readonly pathProcessor = new PathProcessor()
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Creates a function that determines if a request should be handled by this proxy
|
|
39
|
-
*/
|
|
40
|
-
public shouldExec =
|
|
41
|
-
(sourceBaseUrl: string) =>
|
|
42
|
-
({ req }: { req: Pick<IncomingMessage, 'url' | 'method'> }) =>
|
|
43
|
-
req.url ? PathHelper.matchesBaseUrl(req.url, sourceBaseUrl) : false
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Creates an HTTP request handler for the proxy
|
|
47
|
-
*/
|
|
48
|
-
private createRequestHandler(options: ProxyOptions) {
|
|
49
|
-
const handler = new HttpProxyHandler({
|
|
50
|
-
sourceBaseUrl: options.sourceBaseUrl,
|
|
51
|
-
targetBaseUrl: options.targetBaseUrl,
|
|
52
|
-
pathRewrite: options.pathRewrite,
|
|
53
|
-
headers: options.headers,
|
|
54
|
-
cookies: options.cookies,
|
|
55
|
-
responseCookies: options.responseCookies,
|
|
56
|
-
timeout: options.timeout,
|
|
57
|
-
onError: (error) => this.emit('onProxyFailed', error),
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
return async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => {
|
|
61
|
-
await handler.handle(req, res)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Creates a WebSocket upgrade handler for the proxy
|
|
67
|
-
*/
|
|
68
|
-
private createUpgradeHandler(options: ProxyOptions) {
|
|
69
|
-
const handler = new WebSocketProxyHandler({
|
|
70
|
-
sourceBaseUrl: options.sourceBaseUrl,
|
|
71
|
-
targetBaseUrl: options.targetBaseUrl,
|
|
72
|
-
pathRewrite: options.pathRewrite,
|
|
73
|
-
headers: options.headers,
|
|
74
|
-
timeout: options.timeout,
|
|
75
|
-
onError: (error) => this.emit('onWebSocketProxyFailed', error),
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
return async ({ req, socket, head }: { req: IncomingMessage; socket: Duplex; head: Buffer }) => {
|
|
79
|
-
await handler.handle(req, socket, head)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Adds a new proxy configuration
|
|
85
|
-
* @throws Error if targetBaseUrl is invalid or uses non-HTTP/HTTPS protocol
|
|
86
|
-
*/
|
|
87
|
-
public async addProxy(options: ProxyOptions): Promise<void> {
|
|
88
|
-
// Validate targetBaseUrl format
|
|
89
|
-
const url = this.pathProcessor.validateUrl(options.targetBaseUrl, 'targetBaseUrl')
|
|
90
|
-
this.pathProcessor.validateHttpProtocol(url)
|
|
91
|
-
|
|
92
|
-
const server = await this.serverManager.getOrCreate({ hostName: options.sourceHostName, port: options.sourcePort })
|
|
93
|
-
|
|
94
|
-
const api = {
|
|
95
|
-
shouldExec: this.shouldExec(options.sourceBaseUrl),
|
|
96
|
-
onRequest: this.createRequestHandler(options),
|
|
97
|
-
...(options.enableWebsockets ? { onUpgrade: this.createUpgradeHandler(options) } : {}),
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
server.apis.push(api)
|
|
101
|
-
}
|
|
102
|
-
}
|
package/src/server-manager.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@furystack/inject'
|
|
2
|
-
import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
|
|
3
|
-
import type { IncomingMessage, Server, ServerResponse } from 'http'
|
|
4
|
-
import { createServer } from 'http'
|
|
5
|
-
import type { Socket } from 'net'
|
|
6
|
-
import type { Duplex } from 'stream'
|
|
7
|
-
|
|
8
|
-
export interface ServerOptions {
|
|
9
|
-
hostName?: string
|
|
10
|
-
port: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface OnRequest {
|
|
14
|
-
req: IncomingMessage
|
|
15
|
-
res: ServerResponse
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface OnUpgrade {
|
|
19
|
-
req: IncomingMessage
|
|
20
|
-
socket: Duplex
|
|
21
|
-
head: Buffer
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ServerApi {
|
|
25
|
-
shouldExec: (options: OnRequest) => boolean
|
|
26
|
-
onRequest: (options: OnRequest) => Promise<void>
|
|
27
|
-
onUpgrade?: (options: OnUpgrade) => Promise<void>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ServerRecord {
|
|
31
|
-
server: Server
|
|
32
|
-
apis: ServerApi[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@Injectable({ lifetime: 'singleton' })
|
|
36
|
-
export class ServerManager
|
|
37
|
-
extends EventHub<{
|
|
38
|
-
onRequestFailed: [unknown, IncomingMessage, ServerResponse<IncomingMessage>]
|
|
39
|
-
onServerListening: { url: string; port: number; hostName?: string }
|
|
40
|
-
onServerClosed: { url: string }
|
|
41
|
-
onListenerError: ListenerErrorPayload
|
|
42
|
-
}>
|
|
43
|
-
implements AsyncDisposable
|
|
44
|
-
{
|
|
45
|
-
public static DEFAULT_HOST = 'localhost'
|
|
46
|
-
public servers = new Map<string, ServerRecord>()
|
|
47
|
-
private openedSockets = new Set<Socket>()
|
|
48
|
-
private readonly pendingCreates = new Map<string, Promise<ServerRecord>>()
|
|
49
|
-
private getHostUrl = (options: ServerOptions) =>
|
|
50
|
-
`http://${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
|
|
51
|
-
|
|
52
|
-
private onConnection = (socket: Socket) => {
|
|
53
|
-
this.openedSockets.add(socket)
|
|
54
|
-
socket.once('close', () => this.openedSockets.delete(socket))
|
|
55
|
-
}
|
|
56
|
-
public async [Symbol.asyncDispose]() {
|
|
57
|
-
await Promise.allSettled([...this.pendingCreates.values()])
|
|
58
|
-
this.openedSockets.forEach((s) => s.destroy())
|
|
59
|
-
await Promise.allSettled(
|
|
60
|
-
[...this.servers.entries()].map(
|
|
61
|
-
([url, s]) =>
|
|
62
|
-
new Promise<void>((resolve, reject) => {
|
|
63
|
-
s.server.close((err) => {
|
|
64
|
-
if (err) {
|
|
65
|
-
reject(err)
|
|
66
|
-
} else {
|
|
67
|
-
this.emit('onServerClosed', { url })
|
|
68
|
-
resolve()
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
s.server.off('connection', this.onConnection)
|
|
72
|
-
}),
|
|
73
|
-
),
|
|
74
|
-
)
|
|
75
|
-
this.servers.clear()
|
|
76
|
-
super[Symbol.dispose]?.()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public async getOrCreate(options: ServerOptions): Promise<ServerRecord> {
|
|
80
|
-
const url = this.getHostUrl(options)
|
|
81
|
-
|
|
82
|
-
const existing = this.servers.get(url)
|
|
83
|
-
if (existing) {
|
|
84
|
-
return existing
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const pending = this.pendingCreates.get(url)
|
|
88
|
-
if (pending) {
|
|
89
|
-
return pending
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const createPromise = this.createServer(url, options)
|
|
93
|
-
this.pendingCreates.set(url, createPromise)
|
|
94
|
-
return createPromise
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private async createServer(url: string, options: ServerOptions): Promise<ServerRecord> {
|
|
98
|
-
const apis: ServerRecord['apis'] = []
|
|
99
|
-
const server = createServer((req, res) => {
|
|
100
|
-
const apiMatch = apis.find((api) => api.shouldExec({ req, res }))
|
|
101
|
-
if (apiMatch) {
|
|
102
|
-
apiMatch.onRequest({ req, res }).catch((error) => {
|
|
103
|
-
this.emit('onRequestFailed', [error, req, res])
|
|
104
|
-
})
|
|
105
|
-
} else {
|
|
106
|
-
res.destroy()
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
server.on('upgrade', (req, socket, head) => {
|
|
110
|
-
const apiMatch = apis.find((api) => api.shouldExec({ req, res: {} as ServerResponse }))
|
|
111
|
-
if (apiMatch?.onUpgrade) {
|
|
112
|
-
apiMatch.onUpgrade({ req, socket, head }).catch((error) => {
|
|
113
|
-
this.emit('onRequestFailed', [error, req, {} as ServerResponse])
|
|
114
|
-
socket.destroy()
|
|
115
|
-
})
|
|
116
|
-
} else {
|
|
117
|
-
socket.destroy()
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
server.on('connection', this.onConnection)
|
|
121
|
-
try {
|
|
122
|
-
await new Promise<void>((resolve, reject) => {
|
|
123
|
-
server.on('listening', () => resolve())
|
|
124
|
-
server.on('error', (err) => reject(err))
|
|
125
|
-
server.listen(options.port, options.hostName)
|
|
126
|
-
})
|
|
127
|
-
const record: ServerRecord = { server, apis }
|
|
128
|
-
this.servers.set(url, record)
|
|
129
|
-
this.emit('onServerListening', { url, port: options.port, hostName: options.hostName })
|
|
130
|
-
return record
|
|
131
|
-
} finally {
|
|
132
|
-
this.pendingCreates.delete(url)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|