@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,224 +1,137 @@
|
|
|
1
|
-
import { Injector } from '@furystack/inject'
|
|
2
|
-
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
|
-
import { ServerManager } from './server-manager.js'
|
|
4
|
-
import { StaticServerManager } from './static-server-manager.js'
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
6
1
|
import { getPort } from '@furystack/core/port-generator'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { useStaticFiles } from './helpers.js'
|
|
6
|
+
import { HttpServerPoolToken } from './http-server-pool.js'
|
|
7
|
+
import { buildStaticSiteServerApi } from './static-site-runtime.js'
|
|
7
8
|
|
|
8
|
-
describe('
|
|
9
|
+
describe('useStaticFiles / buildStaticSiteServerApi', () => {
|
|
9
10
|
describe('Top level routing', () => {
|
|
10
|
-
it('
|
|
11
|
-
await usingAsync(
|
|
12
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
11
|
+
it('responds 404 without a fallback', async () => {
|
|
12
|
+
await usingAsync(createInjector(), async (i) => {
|
|
13
13
|
const port = getPort()
|
|
14
|
-
await
|
|
15
|
-
baseUrl: '/',
|
|
16
|
-
path: '.',
|
|
17
|
-
port,
|
|
18
|
-
})
|
|
14
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
19
15
|
|
|
20
16
|
const result = await fetch(`http://127.0.0.1:${port}/not-found.html`)
|
|
21
17
|
expect(result.ok).toBe(false)
|
|
22
18
|
expect(result.status).toBe(404)
|
|
23
|
-
expect(result
|
|
24
|
-
|
|
25
|
-
expect(body).toBe('Not found')
|
|
19
|
+
expect(result.headers.get('content-type')).toBe('text/plain')
|
|
20
|
+
expect(await result.text()).toBe('Not found')
|
|
26
21
|
})
|
|
27
22
|
})
|
|
28
23
|
|
|
29
|
-
it('
|
|
30
|
-
await usingAsync(
|
|
31
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
24
|
+
it('falls back to the configured file and returns its content-type', async () => {
|
|
25
|
+
await usingAsync(createInjector(), async (i) => {
|
|
32
26
|
const port = getPort()
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
await useStaticFiles({
|
|
28
|
+
injector: i,
|
|
35
29
|
baseUrl: '/',
|
|
36
30
|
path: '.',
|
|
37
31
|
fallback: 'package.json',
|
|
38
32
|
port,
|
|
39
|
-
headers: {
|
|
40
|
-
'custom-header': 'custom-value',
|
|
41
|
-
},
|
|
33
|
+
headers: { 'custom-header': 'custom-value' },
|
|
42
34
|
})
|
|
43
35
|
|
|
44
36
|
const result = await fetch(`http://localhost:${port}/not-found.html`)
|
|
45
|
-
|
|
46
37
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
47
38
|
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
48
39
|
})
|
|
49
40
|
})
|
|
50
41
|
|
|
51
|
-
it('
|
|
52
|
-
await usingAsync(
|
|
53
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
42
|
+
it('falls back for the root url when no index exists', async () => {
|
|
43
|
+
await usingAsync(createInjector(), async (i) => {
|
|
54
44
|
const port = getPort()
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
await useStaticFiles({
|
|
46
|
+
injector: i,
|
|
57
47
|
baseUrl: '/',
|
|
58
48
|
path: '.',
|
|
59
49
|
fallback: 'package.json',
|
|
60
50
|
port,
|
|
61
|
-
headers: {
|
|
62
|
-
'custom-header': 'custom-value',
|
|
63
|
-
},
|
|
51
|
+
headers: { 'custom-header': 'custom-value' },
|
|
64
52
|
})
|
|
65
53
|
|
|
66
54
|
const result = await fetch(`http://localhost:${port}`)
|
|
67
|
-
|
|
68
55
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
69
56
|
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
70
57
|
})
|
|
71
58
|
})
|
|
72
59
|
|
|
73
|
-
it('
|
|
74
|
-
await usingAsync(
|
|
75
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
60
|
+
it('serves an existing file from the root directory', async () => {
|
|
61
|
+
await usingAsync(createInjector(), async (i) => {
|
|
76
62
|
const port = getPort()
|
|
77
|
-
|
|
78
|
-
await staticServerManager.addStaticSite({
|
|
79
|
-
baseUrl: '/',
|
|
80
|
-
path: '.',
|
|
81
|
-
port,
|
|
82
|
-
headers: {
|
|
83
|
-
'custom-header': 'custom-value',
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
63
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
87
64
|
const result = await fetch(`http://localhost:${port}/README.md`)
|
|
88
|
-
|
|
89
65
|
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
90
|
-
expect(result.headers.get('custom-header')).toBe('custom-value')
|
|
91
66
|
})
|
|
92
67
|
})
|
|
93
68
|
|
|
94
|
-
it('
|
|
95
|
-
await usingAsync(
|
|
96
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
69
|
+
it('serves files from subdirectories', async () => {
|
|
70
|
+
await usingAsync(createInjector(), async (i) => {
|
|
97
71
|
const port = getPort()
|
|
98
|
-
|
|
99
|
-
await staticServerManager.addStaticSite({
|
|
100
|
-
baseUrl: '/',
|
|
101
|
-
path: '.',
|
|
102
|
-
port,
|
|
103
|
-
})
|
|
104
|
-
|
|
72
|
+
await useStaticFiles({ injector: i, baseUrl: '/', path: '.', port })
|
|
105
73
|
const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
|
|
106
|
-
|
|
107
74
|
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
108
75
|
})
|
|
109
76
|
})
|
|
110
77
|
})
|
|
111
78
|
|
|
112
79
|
describe('Non-top level routing', () => {
|
|
113
|
-
it('
|
|
114
|
-
await usingAsync(
|
|
115
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
80
|
+
it('leaves requests outside the base url to the next api in the chain', async () => {
|
|
81
|
+
await usingAsync(createInjector(), async (i) => {
|
|
116
82
|
const port = getPort()
|
|
83
|
+
const serverApi = await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
|
|
117
84
|
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const server = [...injector.getInstance(ServerManager).servers.values()][0]
|
|
125
|
-
|
|
126
|
-
server.apis[0].onRequest = vi.fn()
|
|
85
|
+
const record = await i.get(HttpServerPoolToken).acquire({ port })
|
|
86
|
+
const onRequest = vi.fn()
|
|
87
|
+
// Swap in a spy to observe non-matching requests never reach the handler.
|
|
88
|
+
serverApi.onRequest = onRequest
|
|
89
|
+
record.apis[0] = serverApi
|
|
127
90
|
|
|
128
91
|
fetch(`http://localhost:${port}/bundleToAnotherFolder/not-found.html`).catch(() => {
|
|
129
|
-
|
|
92
|
+
// Expected to fail: the server has no matching API and drops the socket.
|
|
130
93
|
})
|
|
131
|
-
|
|
132
94
|
await sleepAsync(100)
|
|
133
|
-
|
|
134
|
-
expect(server.apis[0].onRequest).not.toHaveBeenCalled()
|
|
95
|
+
expect(onRequest).not.toHaveBeenCalled()
|
|
135
96
|
})
|
|
136
97
|
})
|
|
137
98
|
|
|
138
|
-
it('
|
|
139
|
-
await usingAsync(
|
|
140
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
99
|
+
it('returns 404 under a sub-base url when the file is missing and no fallback is set', async () => {
|
|
100
|
+
await usingAsync(createInjector(), async (i) => {
|
|
141
101
|
const port = getPort()
|
|
142
|
-
|
|
143
|
-
await staticServerManager.addStaticSite({
|
|
144
|
-
baseUrl: '/bundle',
|
|
145
|
-
path: '.',
|
|
146
|
-
port,
|
|
147
|
-
})
|
|
102
|
+
await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', port })
|
|
148
103
|
|
|
149
104
|
const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
|
|
150
105
|
expect(result.ok).toBe(false)
|
|
151
106
|
expect(result.status).toBe(404)
|
|
152
107
|
expect(result.headers.get('content-type')).toBe('text/plain')
|
|
153
|
-
|
|
154
|
-
expect(body).toBe('Not found')
|
|
108
|
+
expect(await result.text()).toBe('Not found')
|
|
155
109
|
})
|
|
156
110
|
})
|
|
157
111
|
|
|
158
|
-
it('
|
|
159
|
-
await usingAsync(
|
|
160
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
112
|
+
it('serves the configured fallback under a sub-base url', async () => {
|
|
113
|
+
await usingAsync(createInjector(), async (i) => {
|
|
161
114
|
const port = getPort()
|
|
162
|
-
|
|
163
|
-
await staticServerManager.addStaticSite({
|
|
164
|
-
baseUrl: '/bundle',
|
|
165
|
-
path: '.',
|
|
166
|
-
fallback: 'package.json',
|
|
167
|
-
port,
|
|
168
|
-
})
|
|
115
|
+
await useStaticFiles({ injector: i, baseUrl: '/bundle', path: '.', fallback: 'package.json', port })
|
|
169
116
|
|
|
170
117
|
const result = await fetch(`http://localhost:${port}/bundle/not-found.html`)
|
|
171
|
-
|
|
172
118
|
expect(result.headers.get('content-type')).toBe('application/json')
|
|
173
119
|
})
|
|
174
120
|
})
|
|
175
|
-
|
|
176
|
-
it('Should return a defined file from a root directory', async () => {
|
|
177
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
178
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
179
|
-
const port = getPort()
|
|
180
|
-
|
|
181
|
-
await staticServerManager.addStaticSite({
|
|
182
|
-
baseUrl: '/',
|
|
183
|
-
path: '.',
|
|
184
|
-
port,
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
const result = await fetch(`http://localhost:${port}/README.md`)
|
|
188
|
-
|
|
189
|
-
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('Should return a defined file from a subdirectory', async () => {
|
|
194
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
195
|
-
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
196
|
-
const port = getPort()
|
|
197
|
-
|
|
198
|
-
await staticServerManager.addStaticSite({
|
|
199
|
-
baseUrl: '/',
|
|
200
|
-
path: '.',
|
|
201
|
-
port,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
const result = await fetch(`http://localhost:${port}/packages/utils/README.md`)
|
|
205
|
-
|
|
206
|
-
expect(result.headers.get('content-type')).toBe('text/markdown')
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
121
|
})
|
|
210
122
|
|
|
211
123
|
describe('shouldExec', () => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
124
|
+
const api = (baseUrl: string) => buildStaticSiteServerApi({ baseUrl, path: '.', port: 0 })
|
|
125
|
+
|
|
126
|
+
it('does not match when the request method is missing', () => {
|
|
127
|
+
expect(api('/').shouldExec({ req: { url: '/' } as never, res: {} as never })).toBe(false)
|
|
215
128
|
})
|
|
216
129
|
|
|
217
|
-
it('
|
|
218
|
-
|
|
219
|
-
expect(staticServerManager.shouldExec('/')({ req: { method: 'POST', url: '/' } })).toBe(false)
|
|
130
|
+
it('does not match non-GET methods', () => {
|
|
131
|
+
expect(api('/').shouldExec({ req: { method: 'POST', url: '/' } as never, res: {} as never })).toBe(false)
|
|
220
132
|
})
|
|
221
|
-
|
|
133
|
+
|
|
134
|
+
const positiveCases: Array<[string, string]> = [
|
|
222
135
|
['/', '/'],
|
|
223
136
|
['/', '/index.html'],
|
|
224
137
|
['/', '/subdir'],
|
|
@@ -228,22 +141,21 @@ describe('StaticServerManager', () => {
|
|
|
228
141
|
['/subdir', '/subdir/'],
|
|
229
142
|
['/subdir', '/subdir/file.js'],
|
|
230
143
|
['/subdir', '/subdir/s2/file.js'],
|
|
231
|
-
]
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
expect(shouldExec).toBe(true)
|
|
144
|
+
]
|
|
145
|
+
positiveCases.forEach(([baseUrl, url]) =>
|
|
146
|
+
it(`matches GET '${url}' against base '${baseUrl}'`, () => {
|
|
147
|
+
expect(api(baseUrl).shouldExec({ req: { method: 'GET', url } as never, res: {} as never })).toBe(true)
|
|
236
148
|
}),
|
|
237
149
|
)
|
|
238
150
|
|
|
239
|
-
it('
|
|
240
|
-
const
|
|
241
|
-
expect(
|
|
242
|
-
expect(
|
|
151
|
+
it('does not match unrelated urls under a non-root base', () => {
|
|
152
|
+
const subdir = api('/subdir')
|
|
153
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/' } as never, res: {} as never })).toBe(false)
|
|
154
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/other/index.html' } as never, res: {} as never })).toBe(
|
|
243
155
|
false,
|
|
244
156
|
)
|
|
245
|
-
expect(
|
|
246
|
-
expect(
|
|
157
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2' } as never, res: {} as never })).toBe(false)
|
|
158
|
+
expect(subdir.shouldExec({ req: { method: 'GET', url: '/subdir2/index.html' } as never, res: {} as never })).toBe(
|
|
247
159
|
false,
|
|
248
160
|
)
|
|
249
161
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { buildStaticSiteServerApi } from './static-site-runtime.js'
|
|
4
|
+
|
|
5
|
+
const makeRequest = (url: string | undefined, method: string | undefined = 'GET'): IncomingMessage =>
|
|
6
|
+
({ url, method, headers: {} }) as unknown as IncomingMessage
|
|
7
|
+
|
|
8
|
+
// Full onRequest / sendFile behaviour (streaming, fallback, 404) is covered
|
|
9
|
+
// end-to-end via `static-server-manager.spec.ts` which drives the real HTTP
|
|
10
|
+
// pool. These tests cover the pure-function `shouldExec` gate which is
|
|
11
|
+
// exercised by the server pool before any request is handed to the API.
|
|
12
|
+
describe('buildStaticSiteServerApi / shouldExec', () => {
|
|
13
|
+
const api = buildStaticSiteServerApi({ baseUrl: '/static', path: '/tmp', port: 0 })
|
|
14
|
+
|
|
15
|
+
it('matches GET requests under the baseUrl', () => {
|
|
16
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html'), res: {} as ServerResponse })).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('is case-insensitive about the method (GET only)', () => {
|
|
20
|
+
expect(api.shouldExec({ req: makeRequest('/static/a.html', 'get'), res: {} as ServerResponse })).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects non-GET methods', () => {
|
|
24
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html', 'POST'), res: {} as ServerResponse })).toBe(false)
|
|
25
|
+
expect(api.shouldExec({ req: makeRequest('/static/index.html', 'DELETE'), res: {} as ServerResponse })).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects URLs that do not match the baseUrl', () => {
|
|
29
|
+
expect(api.shouldExec({ req: makeRequest('/other/index.html'), res: {} as ServerResponse })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects requests without a URL', () => {
|
|
33
|
+
expect(api.shouldExec({ req: makeRequest(undefined), res: {} as ServerResponse })).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('never exposes an onUpgrade hook (static sites do not handle upgrades)', () => {
|
|
37
|
+
expect(api.onUpgrade).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PathHelper } from '@furystack/utils'
|
|
2
|
+
import { createReadStream } from 'fs'
|
|
3
|
+
import { access, stat } from 'fs/promises'
|
|
4
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
|
|
5
|
+
import { join, normalize, sep } from 'path'
|
|
6
|
+
import type { ServerApi } from './http-server-pool.js'
|
|
7
|
+
import { getMimeForFile } from './mime-types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options accepted by {@link buildStaticSiteServerApi} and
|
|
11
|
+
* {@link useStaticFiles}.
|
|
12
|
+
*/
|
|
13
|
+
export interface StaticServerOptions {
|
|
14
|
+
baseUrl: string
|
|
15
|
+
path: string
|
|
16
|
+
hostName?: string
|
|
17
|
+
port: number
|
|
18
|
+
fallback?: string
|
|
19
|
+
headers?: OutgoingHttpHeaders
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sendFile = async ({
|
|
23
|
+
fullPath,
|
|
24
|
+
headers,
|
|
25
|
+
res,
|
|
26
|
+
}: {
|
|
27
|
+
fullPath: string
|
|
28
|
+
res: ServerResponse
|
|
29
|
+
headers?: OutgoingHttpHeaders
|
|
30
|
+
}): Promise<void> => {
|
|
31
|
+
const { size } = await stat(fullPath)
|
|
32
|
+
await access(fullPath)
|
|
33
|
+
const head = {
|
|
34
|
+
...headers,
|
|
35
|
+
'Content-Length': size,
|
|
36
|
+
'Content-Type': getMimeForFile(fullPath),
|
|
37
|
+
}
|
|
38
|
+
Object.entries(head).forEach(([key, value]) => res.setHeader(key, value))
|
|
39
|
+
await new Promise<void>((resolve, reject) =>
|
|
40
|
+
createReadStream(fullPath, { autoClose: true }).once('finish', resolve).once('error', reject).pipe(res),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds a {@link ServerApi} that serves files under `options.path` for every
|
|
46
|
+
* GET request whose URL matches `options.baseUrl`. Falls back to
|
|
47
|
+
* `options.fallback` (if set) for missing files, otherwise responds with 404.
|
|
48
|
+
*/
|
|
49
|
+
export const buildStaticSiteServerApi = (options: StaticServerOptions): ServerApi => {
|
|
50
|
+
const onRequest = async ({ req, res }: { req: IncomingMessage; res: ServerResponse }): Promise<void> => {
|
|
51
|
+
const extractedPath = PathHelper.extractPath(req.url as string, options.baseUrl)
|
|
52
|
+
const filePath = (extractedPath || '/').replaceAll('/', sep)
|
|
53
|
+
const fullPath = normalize(join(options.path, filePath))
|
|
54
|
+
try {
|
|
55
|
+
await sendFile({ fullPath, res, headers: options.headers })
|
|
56
|
+
} catch {
|
|
57
|
+
if (options.fallback) {
|
|
58
|
+
await sendFile({ fullPath: normalize(join(options.path, options.fallback)), res, headers: options.headers })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
62
|
+
res.end('Not found')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
shouldExec: ({ req }) =>
|
|
68
|
+
req.url !== undefined &&
|
|
69
|
+
req.method?.toUpperCase() === 'GET' &&
|
|
70
|
+
PathHelper.matchesBaseUrl(req.url, options.baseUrl)
|
|
71
|
+
? true
|
|
72
|
+
: false,
|
|
73
|
+
onRequest,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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,68 @@
|
|
|
1
|
+
import { defineStore, type StoreToken, User } from '@furystack/core'
|
|
2
|
+
import { defineDataSet, type DataSetToken } from '@furystack/repository'
|
|
3
|
+
import { DefaultSession } from './models/default-session.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown by the default factories of {@link UserStore} and
|
|
7
|
+
* {@link SessionStore} when they are resolved without a caller-provided
|
|
8
|
+
* override. Users and sessions must outlive a process restart, so the
|
|
9
|
+
* rest-service refuses to silently fall back to an in-memory store.
|
|
10
|
+
*/
|
|
11
|
+
export class HttpAuthenticationStoreNotConfiguredError extends Error {
|
|
12
|
+
constructor(storeName: string) {
|
|
13
|
+
super(
|
|
14
|
+
`HTTP authentication store '${storeName}' has not been configured. Bind a persistent PhysicalStore before resolving the HttpUserContext.`,
|
|
15
|
+
)
|
|
16
|
+
this.name = 'HttpAuthenticationStoreNotConfiguredError'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Store token for persisted {@link User} records. Applications must bind
|
|
22
|
+
* this token to a persistent implementation before resolving
|
|
23
|
+
* {@link HttpUserContext} (or any service that derives from it).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* injector.bind(UserStore, () => myPersistentUserStore)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const UserStore: StoreToken<User, 'username'> = defineStore({
|
|
31
|
+
name: 'furystack/rest-service/UserStore',
|
|
32
|
+
model: User,
|
|
33
|
+
primaryKey: 'username',
|
|
34
|
+
factory: () => {
|
|
35
|
+
throw new HttpAuthenticationStoreNotConfiguredError('UserStore')
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Store token for persisted {@link DefaultSession} records. Shares the
|
|
41
|
+
* throw-by-default contract with {@link UserStore}.
|
|
42
|
+
*/
|
|
43
|
+
export const SessionStore: StoreToken<DefaultSession, 'sessionId'> = defineStore({
|
|
44
|
+
name: 'furystack/rest-service/SessionStore',
|
|
45
|
+
model: DefaultSession,
|
|
46
|
+
primaryKey: 'sessionId',
|
|
47
|
+
factory: () => {
|
|
48
|
+
throw new HttpAuthenticationStoreNotConfiguredError('SessionStore')
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Data set token over {@link UserStore}. Consumers of the authentication
|
|
54
|
+
* subsystem (cookie login, JWT authentication plugins, `HttpUserContext`)
|
|
55
|
+
* route all user I/O through this dataset.
|
|
56
|
+
*/
|
|
57
|
+
export const UserDataSet: DataSetToken<User, 'username'> = defineDataSet({
|
|
58
|
+
name: 'furystack/rest-service/UserDataSet',
|
|
59
|
+
store: UserStore,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Data set token over {@link SessionStore}.
|
|
64
|
+
*/
|
|
65
|
+
export const SessionDataSet: DataSetToken<DefaultSession, 'sessionId'> = defineDataSet({
|
|
66
|
+
name: 'furystack/rest-service/SessionDataSet',
|
|
67
|
+
store: SessionStore,
|
|
68
|
+
})
|