@furystack/rest-service 13.0.0 → 14.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/CHANGELOG.md +124 -0
- package/README.md +47 -28
- package/esm/actions/error-action.d.ts +7 -2
- package/esm/actions/error-action.d.ts.map +1 -1
- package/esm/actions/error-action.js +7 -2
- package/esm/actions/error-action.js.map +1 -1
- package/esm/actions/get-current-user.d.ts +1 -4
- package/esm/actions/get-current-user.d.ts.map +1 -1
- package/esm/actions/get-current-user.js +1 -4
- package/esm/actions/get-current-user.js.map +1 -1
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.d.ts +3 -4
- package/esm/actions/is-authenticated.d.ts.map +1 -1
- package/esm/actions/is-authenticated.js +3 -4
- package/esm/actions/is-authenticated.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.d.ts +2 -6
- package/esm/actions/logout.d.ts.map +1 -1
- package/esm/actions/logout.js +3 -7
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/not-found-action.d.ts +1 -3
- package/esm/actions/not-found-action.d.ts.map +1 -1
- package/esm/actions/not-found-action.js +1 -3
- package/esm/actions/not-found-action.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +5 -12
- package/esm/actions/password-login-action.d.ts.map +1 -1
- package/esm/actions/password-login-action.js +6 -13
- 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.d.ts +5 -11
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +6 -12
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +17 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.js +7 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.js +8 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
- package/esm/authorize.d.ts +5 -17
- package/esm/authorize.d.ts.map +1 -1
- package/esm/authorize.js +5 -17
- package/esm/authorize.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 -1
- 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 -1
- 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/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.js +1 -0
- package/esm/get-schema-from-api.js.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +54 -88
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +90 -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 +56 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +27 -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 +34 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +75 -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 +226 -332
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/identity-cache-keys.d.ts +28 -0
- package/esm/identity-cache-keys.d.ts.map +1 -0
- package/esm/identity-cache-keys.js +13 -0
- package/esm/identity-cache-keys.js.map +1 -0
- package/esm/identity-cache-keys.spec.d.ts +2 -0
- package/esm/identity-cache-keys.spec.d.ts.map +1 -0
- package/esm/identity-cache-keys.spec.js +21 -0
- package/esm/identity-cache-keys.spec.js.map +1 -0
- package/esm/identity-event-bus.d.ts +102 -0
- package/esm/identity-event-bus.d.ts.map +1 -0
- package/esm/identity-event-bus.js +151 -0
- package/esm/identity-event-bus.js.map +1 -0
- package/esm/identity-event-bus.spec.d.ts +2 -0
- package/esm/identity-event-bus.spec.d.ts.map +1 -0
- package/esm/identity-event-bus.spec.js +392 -0
- package/esm/identity-event-bus.spec.js.map +1 -0
- package/esm/index.d.ts +9 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +9 -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.spec.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/request-action-implementation.d.ts +25 -0
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/request-action-implementation.js +9 -0
- package/esm/request-action-implementation.js.map +1 -1
- 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 +57 -80
- 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 +49 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +25 -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-resolution-cache.d.ts +65 -0
- package/esm/user-resolution-cache.d.ts.map +1 -0
- package/esm/user-resolution-cache.js +101 -0
- package/esm/user-resolution-cache.js.map +1 -0
- package/esm/user-resolution-cache.spec.d.ts +2 -0
- package/esm/user-resolution-cache.spec.d.ts.map +1 -0
- package/esm/user-resolution-cache.spec.js +220 -0
- package/esm/user-resolution-cache.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.d.ts +13 -12
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +27 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.js +12 -0
- package/esm/validate.js.map +1 -1
- package/package.json +14 -12
- package/src/actions/error-action.ts +7 -3
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/get-current-user.ts +1 -4
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/is-authenticated.ts +3 -4
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +3 -7
- package/src/actions/not-found-action.ts +1 -3
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +6 -13
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +6 -12
- package/src/authentication-providers/authentication-provider.ts +17 -0
- package/src/authentication-providers/basic-auth-provider.ts +7 -0
- package/src/authentication-providers/cookie-auth-provider.ts +8 -0
- package/src/authorize.spec.ts +37 -39
- package/src/authorize.ts +6 -17
- 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 -2
- 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/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +2 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +78 -24
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +246 -378
- package/src/http-user-context.ts +122 -92
- package/src/identity-cache-keys.spec.ts +24 -0
- package/src/identity-cache-keys.ts +31 -0
- package/src/identity-event-bus.spec.ts +285 -0
- package/src/identity-event-bus.ts +230 -0
- package/src/index.ts +9 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +14 -12
- package/src/openapi/generate-openapi-document.spec.ts +2 -2
- package/src/proxy-manager.spec.ts +158 -188
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/request-action-implementation.ts +25 -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 +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +43 -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-resolution-cache.spec.ts +241 -0
- package/src/user-resolution-cache.ts +164 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +69 -0
- package/src/validate.integration.spec.ts +40 -35
- package/src/validate.ts +14 -12
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- 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/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { User } from '@furystack/core'
|
|
2
|
+
import { Cache } from '@furystack/cache'
|
|
3
|
+
import { defineService, type Token } from '@furystack/inject'
|
|
4
|
+
import type { IncomingMessage } from 'http'
|
|
5
|
+
import type { AuthenticationProvider } from './authentication-providers/authentication-provider.js'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_USER_CACHE_CAPACITY,
|
|
8
|
+
DEFAULT_USER_CACHE_TTL_MS,
|
|
9
|
+
HttpAuthenticationSettings,
|
|
10
|
+
} from './http-authentication-settings.js'
|
|
11
|
+
import type { HttpUserContext } from './http-user-context.js'
|
|
12
|
+
import { userCacheTag, type UserCacheTag } from './identity-cache-keys.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Singleton TTL cache that memoizes successful identity resolutions across
|
|
16
|
+
* requests within a single process.
|
|
17
|
+
*
|
|
18
|
+
* Cache keys are produced by the configured {@link AuthenticationProvider}s
|
|
19
|
+
* via their `getCacheKey` method (e.g. cookie-auth keys by session id). The
|
|
20
|
+
* cache's `load` callback runs the same provider chain that
|
|
21
|
+
* {@link HttpUserContext.authenticateRequest} would run — it is supplied via
|
|
22
|
+
* an internal "loader" registered when the {@link HttpUserContext} is
|
|
23
|
+
* resolved.
|
|
24
|
+
*
|
|
25
|
+
* Entries expire after `HttpAuthenticationSettings.userCacheTtlMs`; capacity
|
|
26
|
+
* is bounded by `HttpAuthenticationSettings.userCacheCapacity`. Setting the
|
|
27
|
+
* TTL to `0` disables caching (the loader runs on every call).
|
|
28
|
+
*
|
|
29
|
+
* **Multi-node note:** session invalidation on one instance does not
|
|
30
|
+
* propagate to siblings — the TTL bounds the staleness window. Apps that
|
|
31
|
+
* need stricter freshness must shorten the TTL or invalidate explicitly via
|
|
32
|
+
* {@link UserResolutionCache.invalidate} /
|
|
33
|
+
* {@link UserResolutionCache.invalidateByUser}.
|
|
34
|
+
*/
|
|
35
|
+
export interface UserResolutionCache extends Disposable {
|
|
36
|
+
/**
|
|
37
|
+
* Returns the cached or freshly-resolved `User` for the supplied
|
|
38
|
+
* cache key. The `loader` is invoked on cache miss; concurrent calls for
|
|
39
|
+
* the same key dedupe to a single loader invocation.
|
|
40
|
+
*/
|
|
41
|
+
resolve(cacheKey: string, loader: () => Promise<User>): Promise<User>
|
|
42
|
+
/** Drops a single cache entry. No-op when the key is unknown. */
|
|
43
|
+
invalidate(cacheKey: string): void
|
|
44
|
+
/**
|
|
45
|
+
* Drops every cached entry that resolved to `username`, regardless of
|
|
46
|
+
* which {@link AuthenticationProvider}'s key originally populated it.
|
|
47
|
+
* Backed by the cache's reverse tag index — no per-call O(n) scan.
|
|
48
|
+
*
|
|
49
|
+
* Use after role / password / account-state changes that must take
|
|
50
|
+
* effect immediately on the next request, instead of waiting for the
|
|
51
|
+
* TTL window to elapse.
|
|
52
|
+
*/
|
|
53
|
+
invalidateByUser(username: string): void
|
|
54
|
+
/** Drops every cached entry. */
|
|
55
|
+
invalidateAll(): void
|
|
56
|
+
/** Returns the current entry count (for diagnostics / tests). */
|
|
57
|
+
readonly size: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class UserResolutionCacheImpl implements UserResolutionCache {
|
|
61
|
+
private readonly loaders = new Map<string, () => Promise<User>>()
|
|
62
|
+
private readonly cache: Cache<User, [string], UserCacheTag> | null
|
|
63
|
+
|
|
64
|
+
constructor(ttlMs: number, capacity: number) {
|
|
65
|
+
if (ttlMs <= 0) {
|
|
66
|
+
this.cache = null
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
this.cache = new Cache<User, [string], UserCacheTag>({
|
|
70
|
+
load: (cacheKey) => {
|
|
71
|
+
const loader = this.loaders.get(cacheKey)
|
|
72
|
+
if (!loader) {
|
|
73
|
+
// The loader is only registered for the duration of a `resolve()`
|
|
74
|
+
// call. A reload triggered via `cache.get(key)` outside that call
|
|
75
|
+
// (e.g. from `getObservable`) would have no loader; refuse the
|
|
76
|
+
// load instead of caching `null`.
|
|
77
|
+
return Promise.reject(new Error(`No loader registered for cache key '${cacheKey}'`))
|
|
78
|
+
}
|
|
79
|
+
return loader()
|
|
80
|
+
},
|
|
81
|
+
cacheTimeMs: ttlMs,
|
|
82
|
+
capacity,
|
|
83
|
+
getKey: (cacheKey) => cacheKey,
|
|
84
|
+
getTags: (user) => [userCacheTag(user.username)],
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async resolve(cacheKey: string, loader: () => Promise<User>): Promise<User> {
|
|
89
|
+
if (!this.cache) {
|
|
90
|
+
return loader()
|
|
91
|
+
}
|
|
92
|
+
this.loaders.set(cacheKey, loader)
|
|
93
|
+
try {
|
|
94
|
+
return await this.cache.get(cacheKey)
|
|
95
|
+
} finally {
|
|
96
|
+
// Loader is per-request; clearing prevents the cache from re-running it
|
|
97
|
+
// out-of-band when the entry transitions to obsolete and a sibling call
|
|
98
|
+
// queries `getObservable`.
|
|
99
|
+
this.loaders.delete(cacheKey)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public invalidate(cacheKey: string): void {
|
|
104
|
+
this.cache?.remove(cacheKey)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public invalidateByUser(username: string): void {
|
|
108
|
+
this.cache?.removeByTag(userCacheTag(username))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public invalidateAll(): void {
|
|
112
|
+
this.cache?.flushAll()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public get size(): number {
|
|
116
|
+
return this.cache?.getCount() ?? 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public [Symbol.dispose](): void {
|
|
120
|
+
this.cache?.[Symbol.dispose]()
|
|
121
|
+
this.loaders.clear()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* DI token for the singleton {@link UserResolutionCache}. Resolved lazily
|
|
127
|
+
* by {@link HttpUserContext}; sized from {@link HttpAuthenticationSettings}.
|
|
128
|
+
*/
|
|
129
|
+
export const UserResolutionCache: Token<UserResolutionCache, 'singleton'> = defineService({
|
|
130
|
+
name: 'furystack/rest-service/UserResolutionCache',
|
|
131
|
+
lifetime: 'singleton',
|
|
132
|
+
factory: ({ inject, onDispose }): UserResolutionCache => {
|
|
133
|
+
const settings = inject(HttpAuthenticationSettings)
|
|
134
|
+
const ttlMs = settings.userCacheTtlMs ?? DEFAULT_USER_CACHE_TTL_MS
|
|
135
|
+
const capacity = settings.userCacheCapacity ?? DEFAULT_USER_CACHE_CAPACITY
|
|
136
|
+
const impl = new UserResolutionCacheImpl(ttlMs, capacity)
|
|
137
|
+
onDispose(() => {
|
|
138
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper -- onDispose is the teardown hook
|
|
139
|
+
impl[Symbol.dispose]()
|
|
140
|
+
})
|
|
141
|
+
return impl
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolves the configured cache key for the supplied request by walking the
|
|
147
|
+
* authentication providers in order and returning the first non-null key.
|
|
148
|
+
*
|
|
149
|
+
* Returns `null` when no provider opts in to caching (e.g. Basic Auth, or
|
|
150
|
+
* an anonymous request). The caller should bypass the cache in that case.
|
|
151
|
+
*
|
|
152
|
+
* Exported so plugins / tests can derive the same key the cache uses.
|
|
153
|
+
*/
|
|
154
|
+
export const resolveUserCacheKey = (
|
|
155
|
+
settings: HttpAuthenticationSettings,
|
|
156
|
+
request: Pick<IncomingMessage, 'headers'>,
|
|
157
|
+
): string | null => {
|
|
158
|
+
for (const provider of settings.authenticationProviders) {
|
|
159
|
+
if (!provider.getCacheKey) continue
|
|
160
|
+
const key = provider.getCacheKey(request)
|
|
161
|
+
if (key) return key
|
|
162
|
+
}
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { InMemoryStore, User } from '@furystack/core'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { usingAsync } from '@furystack/utils'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { DefaultSession } from './models/default-session.js'
|
|
6
|
+
import {
|
|
7
|
+
HttpAuthenticationStoreNotConfiguredError,
|
|
8
|
+
SessionDataSet,
|
|
9
|
+
SessionStore,
|
|
10
|
+
UserDataSet,
|
|
11
|
+
UserStore,
|
|
12
|
+
} from './user-store.js'
|
|
13
|
+
|
|
14
|
+
describe('UserStore', () => {
|
|
15
|
+
it('exposes the User model and username primary key on the token', () => {
|
|
16
|
+
expect(UserStore.model).toBe(User)
|
|
17
|
+
expect(UserStore.primaryKey).toBe('username')
|
|
18
|
+
expect(UserStore.lifetime).toBe('singleton')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('throws HttpAuthenticationStoreNotConfiguredError when resolved without an override', async () => {
|
|
22
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
23
|
+
expect(() => injector.get(UserStore)).toThrow(HttpAuthenticationStoreNotConfiguredError)
|
|
24
|
+
expect(() => injector.get(UserStore)).toThrow(/UserStore.*not been configured/)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns the bound implementation after rebinding the token', async () => {
|
|
29
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
30
|
+
const impl = new InMemoryStore({ model: User, primaryKey: 'username' })
|
|
31
|
+
injector.bind(UserStore, () => impl)
|
|
32
|
+
expect(injector.get(UserStore)).toBe(impl)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('SessionStore', () => {
|
|
38
|
+
it('exposes the DefaultSession model and sessionId primary key on the token', () => {
|
|
39
|
+
expect(SessionStore.model).toBe(DefaultSession)
|
|
40
|
+
expect(SessionStore.primaryKey).toBe('sessionId')
|
|
41
|
+
expect(SessionStore.lifetime).toBe('singleton')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('throws HttpAuthenticationStoreNotConfiguredError when resolved without an override', async () => {
|
|
45
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
46
|
+
expect(() => injector.get(SessionStore)).toThrow(HttpAuthenticationStoreNotConfiguredError)
|
|
47
|
+
expect(() => injector.get(SessionStore)).toThrow(/SessionStore.*not been configured/)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns the bound implementation after rebinding the token', async () => {
|
|
52
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
53
|
+
const impl = new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' })
|
|
54
|
+
injector.bind(SessionStore, () => impl)
|
|
55
|
+
expect(injector.get(SessionStore)).toBe(impl)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('UserDataSet / SessionDataSet', () => {
|
|
61
|
+
it('mirror the backing store metadata onto the data-set token', () => {
|
|
62
|
+
expect(UserDataSet.model).toBe(User)
|
|
63
|
+
expect(UserDataSet.primaryKey).toBe('username')
|
|
64
|
+
expect(SessionDataSet.model).toBe(DefaultSession)
|
|
65
|
+
expect(SessionDataSet.primaryKey).toBe('sessionId')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('resolves through the bound store implementation', async () => {
|
|
69
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
70
|
+
injector.bind(UserStore, () => new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
71
|
+
const dataSet = injector.get(UserDataSet)
|
|
72
|
+
await dataSet.add(injector, { username: 'alice', roles: [] })
|
|
73
|
+
const result = await dataSet.get(injector, 'alice')
|
|
74
|
+
expect(result?.username).toBe('alice')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('HttpAuthenticationStoreNotConfiguredError', () => {
|
|
80
|
+
it('embeds the store name in the message and sets the error name', () => {
|
|
81
|
+
const error = new HttpAuthenticationStoreNotConfiguredError('SomeStore')
|
|
82
|
+
expect(error.name).toBe('HttpAuthenticationStoreNotConfiguredError')
|
|
83
|
+
expect(error.message).toContain("'SomeStore'")
|
|
84
|
+
expect(error).toBeInstanceOf(Error)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineStore, type StoreToken, User } from '@furystack/core'
|
|
2
|
+
import { defineDataSet, type DataSetToken } from '@furystack/repository'
|
|
3
|
+
import type { HttpUserContext } from './http-user-context.js'
|
|
4
|
+
import { DefaultSession } from './models/default-session.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown by the default factories of {@link UserStore} and
|
|
8
|
+
* {@link SessionStore} when they are resolved without a caller-provided
|
|
9
|
+
* override. Users and sessions must outlive a process restart, so the
|
|
10
|
+
* rest-service refuses to silently fall back to an in-memory store.
|
|
11
|
+
*/
|
|
12
|
+
export class HttpAuthenticationStoreNotConfiguredError extends Error {
|
|
13
|
+
constructor(storeName: string) {
|
|
14
|
+
super(
|
|
15
|
+
`HTTP authentication store '${storeName}' has not been configured. Bind a persistent PhysicalStore before resolving the HttpUserContext.`,
|
|
16
|
+
)
|
|
17
|
+
this.name = 'HttpAuthenticationStoreNotConfiguredError'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Store token for persisted {@link User} records. Applications must bind
|
|
23
|
+
* this token to a persistent implementation before resolving
|
|
24
|
+
* {@link HttpUserContext} (or any service that derives from it).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* injector.bind(UserStore, () => myPersistentUserStore)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const UserStore: StoreToken<User, 'username'> = defineStore({
|
|
32
|
+
name: 'furystack/rest-service/UserStore',
|
|
33
|
+
model: User,
|
|
34
|
+
primaryKey: 'username',
|
|
35
|
+
factory: () => {
|
|
36
|
+
throw new HttpAuthenticationStoreNotConfiguredError('UserStore')
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Store token for persisted {@link DefaultSession} records. Shares the
|
|
42
|
+
* throw-by-default contract with {@link UserStore}.
|
|
43
|
+
*/
|
|
44
|
+
export const SessionStore: StoreToken<DefaultSession, 'sessionId'> = defineStore({
|
|
45
|
+
name: 'furystack/rest-service/SessionStore',
|
|
46
|
+
model: DefaultSession,
|
|
47
|
+
primaryKey: 'sessionId',
|
|
48
|
+
factory: () => {
|
|
49
|
+
throw new HttpAuthenticationStoreNotConfiguredError('SessionStore')
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Data set token over {@link UserStore}. Consumers of the authentication
|
|
55
|
+
* subsystem (cookie login, JWT authentication plugins, `HttpUserContext`)
|
|
56
|
+
* route all user I/O through this dataset.
|
|
57
|
+
*/
|
|
58
|
+
export const UserDataSet: DataSetToken<User, 'username'> = defineDataSet({
|
|
59
|
+
name: 'furystack/rest-service/UserDataSet',
|
|
60
|
+
store: UserStore,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Data set token over {@link SessionStore}.
|
|
65
|
+
*/
|
|
66
|
+
export const SessionDataSet: DataSetToken<DefaultSession, 'sessionId'> = defineDataSet({
|
|
67
|
+
name: 'furystack/rest-service/SessionDataSet',
|
|
68
|
+
store: SessionStore,
|
|
69
|
+
})
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion -- false positives: `client` is typed for a narrow `RestApi`; casts to `ReturnType<typeof createClient<...>>` widen it so this test can exercise endpoints (e.g. `/openapi.json`) the production type intentionally hides */
|
|
3
|
+
import { InMemoryStore, User as UserModel } from '@furystack/core'
|
|
3
4
|
import { getPort } from '@furystack/core/port-generator'
|
|
4
|
-
import {
|
|
5
|
-
import { getRepository } from '@furystack/repository'
|
|
5
|
+
import { createInjector } from '@furystack/inject'
|
|
6
6
|
import type { OpenApiDocument, WithSchemaAction } from '@furystack/rest'
|
|
7
7
|
import { createClient, ResponseError } from '@furystack/rest-client-fetch'
|
|
8
|
+
import {
|
|
9
|
+
PasswordCredential,
|
|
10
|
+
PasswordCredentialStore,
|
|
11
|
+
PasswordResetToken,
|
|
12
|
+
PasswordResetTokenStore,
|
|
13
|
+
usePasswordPolicy,
|
|
14
|
+
} from '@furystack/security'
|
|
8
15
|
import { usingAsync } from '@furystack/utils'
|
|
9
16
|
import type Ajv from 'ajv'
|
|
10
17
|
import { describe, expect, it } from 'vitest'
|
|
@@ -13,10 +20,11 @@ import { createGetCollectionEndpoint } from './endpoint-generators/create-get-co
|
|
|
13
20
|
import { createGetEntityEndpoint } from './endpoint-generators/create-get-entity-endpoint.js'
|
|
14
21
|
import { createPatchEndpoint } from './endpoint-generators/create-patch-endpoint.js'
|
|
15
22
|
import { createPostEndpoint } from './endpoint-generators/create-post-endpoint.js'
|
|
16
|
-
import {
|
|
23
|
+
import { MockDataSet, setupContext } from './endpoint-generators/utils.js'
|
|
17
24
|
import { useHttpAuthentication, useRestService } from './helpers.js'
|
|
18
25
|
import { DefaultSession } from './models/default-session.js'
|
|
19
26
|
import { JsonResult } from './request-action-implementation.js'
|
|
27
|
+
import { SessionStore, UserStore } from './user-store.js'
|
|
20
28
|
import type { ValidationApi } from './validate.integration.schema.js'
|
|
21
29
|
import schema from './validate.integration.spec.schema.json' with { type: 'json' }
|
|
22
30
|
import { Validate } from './validate.js'
|
|
@@ -28,15 +36,12 @@ const description = crypto.randomUUID()
|
|
|
28
36
|
const version = crypto.randomUUID()
|
|
29
37
|
|
|
30
38
|
const createValidateApi = async (options = { enableGetSchema: false }) => {
|
|
31
|
-
const injector =
|
|
39
|
+
const injector = createInjector()
|
|
32
40
|
const port = getPort()
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
getRepository(injector).createDataSet(MockClass, 'id')
|
|
38
|
-
getRepository(injector).createDataSet(User, 'username')
|
|
39
|
-
getRepository(injector).createDataSet(DefaultSession, 'sessionId')
|
|
42
|
+
injector.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
|
|
43
|
+
injector.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
44
|
+
setupContext(injector)
|
|
40
45
|
|
|
41
46
|
const api = await useRestService<ValidationApi>({
|
|
42
47
|
injector,
|
|
@@ -61,11 +66,11 @@ const createValidateApi = async (options = { enableGetSchema: false }) => {
|
|
|
61
66
|
'/mock': Validate({
|
|
62
67
|
schema,
|
|
63
68
|
schemaName: 'GetMockCollectionEndpoint',
|
|
64
|
-
})(createGetCollectionEndpoint(
|
|
69
|
+
})(createGetCollectionEndpoint(MockDataSet)),
|
|
65
70
|
'/mock/:id': Validate({
|
|
66
71
|
schema,
|
|
67
72
|
schemaName: 'GetMockEntityEndpoint',
|
|
68
|
-
})(createGetEntityEndpoint(
|
|
73
|
+
})(createGetEntityEndpoint(MockDataSet)),
|
|
69
74
|
},
|
|
70
75
|
POST: {
|
|
71
76
|
'/validate-body': Validate({
|
|
@@ -78,19 +83,19 @@ const createValidateApi = async (options = { enableGetSchema: false }) => {
|
|
|
78
83
|
'/mock': Validate({
|
|
79
84
|
schema,
|
|
80
85
|
schemaName: 'PostMockEndpoint',
|
|
81
|
-
})(createPostEndpoint(
|
|
86
|
+
})(createPostEndpoint(MockDataSet)),
|
|
82
87
|
},
|
|
83
88
|
PATCH: {
|
|
84
89
|
'/mock/:id': Validate({
|
|
85
90
|
schema,
|
|
86
91
|
schemaName: 'PatchMockEndpoint',
|
|
87
|
-
})(createPatchEndpoint(
|
|
92
|
+
})(createPatchEndpoint(MockDataSet)),
|
|
88
93
|
},
|
|
89
94
|
DELETE: {
|
|
90
95
|
'/mock/:id': Validate({
|
|
91
96
|
schema,
|
|
92
97
|
schemaName: 'DeleteMockEndpoint',
|
|
93
|
-
})(createDeleteEndpoint(
|
|
98
|
+
})(createDeleteEndpoint(MockDataSet)),
|
|
94
99
|
},
|
|
95
100
|
},
|
|
96
101
|
port,
|
|
@@ -447,16 +452,22 @@ describe('Validation integration tests', () => {
|
|
|
447
452
|
|
|
448
453
|
describe('OpenAPI security schemes from authentication providers', () => {
|
|
449
454
|
const createApiWithAuth = async () => {
|
|
450
|
-
const injector =
|
|
455
|
+
const injector = createInjector()
|
|
451
456
|
const port = getPort()
|
|
452
457
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
458
|
+
injector.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
|
|
459
|
+
injector.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
460
|
+
injector.bind(
|
|
461
|
+
PasswordCredentialStore,
|
|
462
|
+
() => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }),
|
|
463
|
+
)
|
|
464
|
+
injector.bind(
|
|
465
|
+
PasswordResetTokenStore,
|
|
466
|
+
() => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }),
|
|
467
|
+
)
|
|
468
|
+
setupContext(injector)
|
|
469
|
+
|
|
470
|
+
usePasswordPolicy(injector)
|
|
460
471
|
useHttpAuthentication(injector)
|
|
461
472
|
|
|
462
473
|
await useRestService<ValidationApi>({
|
|
@@ -477,10 +488,10 @@ describe('Validation integration tests', () => {
|
|
|
477
488
|
JsonResult({ ...headers }),
|
|
478
489
|
),
|
|
479
490
|
'/mock': Validate({ schema, schemaName: 'GetMockCollectionEndpoint' })(
|
|
480
|
-
createGetCollectionEndpoint(
|
|
491
|
+
createGetCollectionEndpoint(MockDataSet),
|
|
481
492
|
),
|
|
482
493
|
'/mock/:id': Validate({ schema, schemaName: 'GetMockEntityEndpoint' })(
|
|
483
|
-
createGetEntityEndpoint(
|
|
494
|
+
createGetEntityEndpoint(MockDataSet),
|
|
484
495
|
),
|
|
485
496
|
},
|
|
486
497
|
POST: {
|
|
@@ -488,19 +499,13 @@ describe('Validation integration tests', () => {
|
|
|
488
499
|
const body = await getBody()
|
|
489
500
|
return JsonResult({ ...body })
|
|
490
501
|
}),
|
|
491
|
-
'/mock': Validate({ schema, schemaName: 'PostMockEndpoint' })(
|
|
492
|
-
createPostEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
493
|
-
),
|
|
502
|
+
'/mock': Validate({ schema, schemaName: 'PostMockEndpoint' })(createPostEndpoint(MockDataSet)),
|
|
494
503
|
},
|
|
495
504
|
PATCH: {
|
|
496
|
-
'/mock/:id': Validate({ schema, schemaName: 'PatchMockEndpoint' })(
|
|
497
|
-
createPatchEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
498
|
-
),
|
|
505
|
+
'/mock/:id': Validate({ schema, schemaName: 'PatchMockEndpoint' })(createPatchEndpoint(MockDataSet)),
|
|
499
506
|
},
|
|
500
507
|
DELETE: {
|
|
501
|
-
'/mock/:id': Validate({ schema, schemaName: 'DeleteMockEndpoint' })(
|
|
502
|
-
createDeleteEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
503
|
-
),
|
|
508
|
+
'/mock/:id': Validate({ schema, schemaName: 'DeleteMockEndpoint' })(createDeleteEndpoint(MockDataSet)),
|
|
504
509
|
},
|
|
505
510
|
},
|
|
506
511
|
port,
|
package/src/validate.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ActionResult, RequestAction, RequestActionOptions } from './request-action-implementation.js'
|
|
2
|
+
import type { SchemaValidationError } from './schema-validator/schema-validation-error.js'
|
|
2
3
|
import { SchemaValidator } from './schema-validator/schema-validator.js'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Represents a JSON Schema definition structure
|
|
6
|
-
*/
|
|
7
5
|
type JsonSchemaDefinition = {
|
|
8
6
|
required?: string[]
|
|
9
7
|
additionalProperties?: boolean
|
|
@@ -17,22 +15,26 @@ type JsonSchemaDefinition = {
|
|
|
17
15
|
[key: string]: unknown
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
/**
|
|
21
|
-
* Represents a JSON Schema with definitions
|
|
22
|
-
*/
|
|
23
18
|
type JsonSchemaWithDefinitions = {
|
|
24
19
|
definitions: Record<string, JsonSchemaDefinition>
|
|
25
20
|
}
|
|
26
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Wraps a {@link RequestAction} with JSON Schema validation. The wrapper
|
|
24
|
+
* pre-validates `query`, `body`, `url` and `headers` against the named
|
|
25
|
+
* schema entry, throws {@link SchemaValidationError} on failure (caught by
|
|
26
|
+
* `ErrorAction` to return 400), then forwards the validated values to the
|
|
27
|
+
* inner action.
|
|
28
|
+
*
|
|
29
|
+
* The wrapped function exposes `schema` and `schemaName` so OpenAPI
|
|
30
|
+
* generators can reflect on the action without re-parsing the source.
|
|
31
|
+
*
|
|
32
|
+
* `furystack/rest-action-validate-wrapper` enforces this on REST actions.
|
|
33
|
+
*/
|
|
27
34
|
export const Validate =
|
|
28
35
|
<TSchema extends JsonSchemaWithDefinitions>(validationOptions: {
|
|
29
|
-
/**
|
|
30
|
-
* The Schema object
|
|
31
|
-
*/
|
|
32
36
|
schema: TSchema
|
|
33
|
-
/**
|
|
34
|
-
* Entity key from the JSON Schema object
|
|
35
|
-
*/
|
|
37
|
+
/** Entry under `schema.definitions` describing this endpoint's request shape. */
|
|
36
38
|
schemaName: keyof TSchema['definitions']
|
|
37
39
|
}) =>
|
|
38
40
|
<T extends { result: unknown }>(
|
package/esm/api-manager.d.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type { Injector } from '@furystack/inject';
|
|
2
|
-
import type { Method, RestApi } from '@furystack/rest';
|
|
3
|
-
import type { MatchFunction } from 'path-to-regexp';
|
|
4
|
-
import type { CorsOptions } from './models/cors-options.js';
|
|
5
|
-
import type { RequestAction } from './request-action-implementation.js';
|
|
6
|
-
import type { OnRequest } from './server-manager.js';
|
|
7
|
-
import './server-response-extensions.js';
|
|
8
|
-
export type RestApiImplementation<T extends RestApi> = {
|
|
9
|
-
[TMethod in keyof T]: {
|
|
10
|
-
[TUrl in keyof T[TMethod]]: T[TMethod][TUrl] extends {
|
|
11
|
-
result: unknown;
|
|
12
|
-
} ? RequestAction<T[TMethod][TUrl]> : never;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
export interface ImplementApiOptions<T extends RestApi> {
|
|
16
|
-
/**
|
|
17
|
-
* The structure of the implemented API.
|
|
18
|
-
*/
|
|
19
|
-
api: RestApiImplementation<T>;
|
|
20
|
-
/**
|
|
21
|
-
* The Injector instance to use for dependency injection in the API actions.
|
|
22
|
-
*/
|
|
23
|
-
injector: Injector;
|
|
24
|
-
/**
|
|
25
|
-
* The host name for the API Server. If not provided, the default host (ServerManager.DEFAULT_HOST) will be used.
|
|
26
|
-
*/
|
|
27
|
-
hostName?: string;
|
|
28
|
-
/**
|
|
29
|
-
* The root path for the API. This will be prepended to all API paths.
|
|
30
|
-
*/
|
|
31
|
-
root: string;
|
|
32
|
-
/**
|
|
33
|
-
* The port on which the API server will listen.
|
|
34
|
-
*/
|
|
35
|
-
port: number;
|
|
36
|
-
/**
|
|
37
|
-
* CORS options to configure Cross-Origin Resource Sharing for the API.
|
|
38
|
-
*/
|
|
39
|
-
cors?: CorsOptions;
|
|
40
|
-
/**
|
|
41
|
-
* An optional function to deserialize query parameters from the URL.
|
|
42
|
-
* This function should take a query string (e.g., "?key=value") and return an object with the parsed parameters.
|
|
43
|
-
* If not provided, the default deserialization will be used.
|
|
44
|
-
*/
|
|
45
|
-
deserializeQueryParams?: (param: string) => any;
|
|
46
|
-
/**
|
|
47
|
-
* Adds an additional 'GET /schema' endpoint that returns the schema definitions of the API.
|
|
48
|
-
* Also adds a 'GET /openapi.json' endpoint that returns the API schema in OpenAPI 3.1 format.
|
|
49
|
-
*/
|
|
50
|
-
enableGetSchema?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Optional name for the API, used in the generated schema.
|
|
53
|
-
* This can be useful for documentation or identification purposes.
|
|
54
|
-
*/
|
|
55
|
-
name?: string;
|
|
56
|
-
/**
|
|
57
|
-
* Optional description for the API, used in the generated schema.
|
|
58
|
-
* This can provide additional context or information about the API's purpose.
|
|
59
|
-
*/
|
|
60
|
-
description?: string;
|
|
61
|
-
/**
|
|
62
|
-
* Optional version for the API, used in the generated schema.
|
|
63
|
-
* This can help in versioning the API and tracking changes over time.
|
|
64
|
-
*/
|
|
65
|
-
version?: string;
|
|
66
|
-
}
|
|
67
|
-
export type NewCompiledApiEntry = {
|
|
68
|
-
method: Method;
|
|
69
|
-
fullPath: string;
|
|
70
|
-
matcher: MatchFunction<Partial<Record<string, string | string[]>>>;
|
|
71
|
-
action: RequestAction<any>;
|
|
72
|
-
};
|
|
73
|
-
export type NewCompiledApi = {
|
|
74
|
-
[K in Method]?: NewCompiledApiEntry[];
|
|
75
|
-
};
|
|
76
|
-
export type OnRequestOptions = OnRequest & {
|
|
77
|
-
compiledApi: NewCompiledApi;
|
|
78
|
-
hostName?: string;
|
|
79
|
-
port: number;
|
|
80
|
-
rootApiPath: string;
|
|
81
|
-
injector: Injector;
|
|
82
|
-
cors?: CorsOptions;
|
|
83
|
-
supportedMethods: string[];
|
|
84
|
-
deserializeQueryParams?: (param: string) => Record<string, unknown>;
|
|
85
|
-
};
|
|
86
|
-
export declare class ApiManager {
|
|
87
|
-
private getSuportedMethods;
|
|
88
|
-
private compileApi;
|
|
89
|
-
addApi<T extends RestApi>({ api, hostName, port, root, cors, injector, deserializeQueryParams, enableGetSchema, name, description, version, }: ImplementApiOptions<T>): Promise<{
|
|
90
|
-
shouldExec: (msg: OnRequest) => boolean;
|
|
91
|
-
onRequest: (msg: OnRequest) => Promise<void>;
|
|
92
|
-
}>;
|
|
93
|
-
shouldExecRequest(options: {
|
|
94
|
-
method?: Method;
|
|
95
|
-
url?: string;
|
|
96
|
-
rootApiPath: string;
|
|
97
|
-
supportedMethods: Method[];
|
|
98
|
-
}): boolean;
|
|
99
|
-
private getActionFromEndpoint;
|
|
100
|
-
private executeAction;
|
|
101
|
-
private onMessage;
|
|
102
|
-
private readonly serverManager;
|
|
103
|
-
}
|
|
104
|
-
//# sourceMappingURL=api-manager.d.ts.map
|
package/esm/api-manager.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api-manager.d.ts","sourceRoot":"","sources":["../src/api-manager.ts"],"names":[],"mappings":"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"}
|