@furystack/rest-service 9.0.0 → 9.0.2
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/esm/actions/login.d.ts +0 -1
- package/esm/actions/login.d.ts.map +1 -1
- package/esm/actions/login.js +0 -1
- package/esm/actions/login.js.map +1 -1
- package/esm/add-cors-header.d.ts +14 -0
- package/esm/add-cors-header.d.ts.map +1 -0
- package/esm/add-cors-header.js +23 -0
- package/esm/add-cors-header.js.map +1 -0
- package/esm/add-cors-header.spec.js +7 -8
- package/esm/add-cors-header.spec.js.map +1 -1
- package/esm/api-manager.d.ts.map +1 -1
- package/esm/api-manager.js +4 -5
- package/esm/api-manager.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +0 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +0 -1
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +4 -2
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +0 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +0 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +16 -10
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +0 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +0 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +10 -6
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +0 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +2 -2
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +4 -2
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +0 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +2 -2
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +4 -2
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +0 -2
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +1 -1
- package/esm/helpers.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +1 -1
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-user-context.d.ts +7 -6
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +17 -5
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.js +60 -2
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/incoming-message-extensions.d.ts +0 -2
- package/esm/incoming-message-extensions.d.ts.map +1 -1
- package/esm/incoming-message-extensions.js +1 -10
- package/esm/incoming-message-extensions.js.map +1 -1
- package/esm/index.d.ts +2 -2
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -2
- package/esm/index.js.map +1 -1
- package/esm/models/default-session.d.ts.map +1 -1
- package/esm/models/default-session.js +0 -8
- package/esm/models/default-session.js.map +1 -1
- package/esm/read-post-body.d.ts +12 -0
- package/esm/read-post-body.d.ts.map +1 -0
- package/esm/read-post-body.js +33 -0
- package/esm/read-post-body.js.map +1 -0
- package/esm/request-action-implementation.d.ts +4 -4
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +24 -33
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +1 -1
- package/esm/static-server-manager.d.ts.map +1 -1
- package/esm/static-server-manager.js +0 -1
- package/esm/static-server-manager.js.map +1 -1
- package/esm/static-server-manager.spec.js +2 -15
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/validate.d.ts +3 -1
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +6 -2
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +13 -13
- package/src/actions/login.ts +0 -1
- package/src/add-cors-header.spec.ts +7 -8
- package/src/add-cors-header.ts +32 -0
- package/src/api-manager.ts +5 -5
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +4 -2
- package/src/endpoint-generators/create-delete-endpoint.ts +0 -1
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +16 -10
- package/src/endpoint-generators/create-get-collection-endpoint.ts +0 -1
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +10 -6
- package/src/endpoint-generators/create-get-entity-endpoint.ts +0 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +4 -2
- package/src/endpoint-generators/create-patch-endpoint.ts +2 -2
- package/src/endpoint-generators/create-post-endpoint.spec.ts +4 -2
- package/src/endpoint-generators/create-post-endpoint.ts +2 -2
- package/src/endpoint-generators/utils.ts +2 -2
- package/src/helpers.ts +1 -1
- package/src/http-authentication-settings.ts +1 -2
- package/src/http-user-context.spec.ts +79 -2
- package/src/http-user-context.ts +29 -10
- package/src/incoming-message-extensions.ts +0 -15
- package/src/index.ts +2 -2
- package/src/models/default-session.ts +2 -2
- package/src/read-post-body.ts +36 -0
- package/src/rest-service.integration.spec.ts +24 -38
- package/src/rest.integration.test.ts +1 -1
- package/src/static-server-manager.spec.ts +2 -18
- package/src/static-server-manager.ts +1 -1
- package/src/validate.integration.spec.ts +9 -2
- package/esm/incoming-message-extensions.spec.d.ts +0 -2
- package/esm/incoming-message-extensions.spec.d.ts.map +0 -1
- package/esm/incoming-message-extensions.spec.js +0 -38
- package/esm/incoming-message-extensions.spec.js.map +0 -1
- package/esm/utils.d.ts +0 -25
- package/esm/utils.d.ts.map +0 -1
- package/esm/utils.js +0 -70
- package/esm/utils.js.map +0 -1
- package/src/incoming-message-extensions.spec.ts +0 -42
- package/src/utils.ts +0 -68
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http'
|
|
2
|
+
import type { CorsOptions } from './models/cors-options.js'
|
|
3
|
+
import type { ServerResponse } from 'http'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds the specified CORS headers to the response
|
|
7
|
+
* @param options The CORS Options object
|
|
8
|
+
* @param incomingMessage The incoming message instance
|
|
9
|
+
* @param serverResponse The outgoing response instance
|
|
10
|
+
*/
|
|
11
|
+
export const addCorsHeaders = (
|
|
12
|
+
options: CorsOptions,
|
|
13
|
+
incomingMessage: IncomingMessage,
|
|
14
|
+
serverResponse: ServerResponse,
|
|
15
|
+
) => {
|
|
16
|
+
if (
|
|
17
|
+
incomingMessage.headers &&
|
|
18
|
+
incomingMessage.headers.origin !== incomingMessage.headers.host &&
|
|
19
|
+
options.origins.some((origin) => origin === incomingMessage.headers.origin)
|
|
20
|
+
) {
|
|
21
|
+
serverResponse.setHeader('Access-Control-Allow-Origin', incomingMessage.headers.origin as string)
|
|
22
|
+
if (options.credentials) {
|
|
23
|
+
serverResponse.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
24
|
+
}
|
|
25
|
+
if (options.headers && options.headers.length) {
|
|
26
|
+
serverResponse.setHeader('Access-Control-Allow-Headers', options.headers.join(', '))
|
|
27
|
+
}
|
|
28
|
+
if (options.methods && options.methods.length) {
|
|
29
|
+
serverResponse.setHeader('Access-Control-Allow-Methods', options.methods.join(', '))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/api-manager.ts
CHANGED
|
@@ -13,11 +13,12 @@ import type { OnRequest } from './server-manager.js'
|
|
|
13
13
|
import { ServerManager } from './server-manager.js'
|
|
14
14
|
import { NotFoundAction } from './actions/not-found-action.js'
|
|
15
15
|
import type { CorsOptions } from './models/cors-options.js'
|
|
16
|
-
import { Utils } from './utils.js'
|
|
17
16
|
import { ErrorAction } from './actions/error-action.js'
|
|
18
17
|
import './server-response-extensions.js'
|
|
19
18
|
import { HttpUserContext } from './http-user-context.js'
|
|
20
19
|
import type { RequestAction } from './request-action-implementation.js'
|
|
20
|
+
import { addCorsHeaders } from './add-cors-header.js'
|
|
21
|
+
import { readPostBody } from './read-post-body.js'
|
|
21
22
|
|
|
22
23
|
export type RestApiImplementation<T extends RestApi> = {
|
|
23
24
|
[TMethod in keyof T]: {
|
|
@@ -167,7 +168,6 @@ export class ApiManager implements Disposable {
|
|
|
167
168
|
params: any
|
|
168
169
|
}) {
|
|
169
170
|
await usingAsync(injector.createChild(), async (i) => {
|
|
170
|
-
const utils = i.getInstance(Utils)
|
|
171
171
|
const httpUserContext = i.getInstance(HttpUserContext)
|
|
172
172
|
i.setExplicitInstance<IdentityContext>(
|
|
173
173
|
{
|
|
@@ -182,7 +182,7 @@ export class ApiManager implements Disposable {
|
|
|
182
182
|
request: req,
|
|
183
183
|
response: res,
|
|
184
184
|
injector: i,
|
|
185
|
-
getBody: () =>
|
|
185
|
+
getBody: () => readPostBody<any>(req),
|
|
186
186
|
headers: req.headers,
|
|
187
187
|
getQuery: () =>
|
|
188
188
|
deserializeQueryParams ? deserializeQueryParams(fullUrl.search) : deserializeQueryString(fullUrl.search),
|
|
@@ -213,7 +213,7 @@ export class ApiManager implements Disposable {
|
|
|
213
213
|
),
|
|
214
214
|
)
|
|
215
215
|
|
|
216
|
-
options.cors &&
|
|
216
|
+
options.cors && addCorsHeaders(options.cors, options.req, options.res)
|
|
217
217
|
if (options.req.method === 'OPTIONS') {
|
|
218
218
|
options.res.writeHead(200)
|
|
219
219
|
options.res.end()
|
|
@@ -231,5 +231,5 @@ export class ApiManager implements Disposable {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
@Injected(ServerManager)
|
|
234
|
-
private readonly serverManager
|
|
234
|
+
private declare readonly serverManager: ServerManager
|
|
235
235
|
}
|
|
@@ -6,15 +6,17 @@ import { MockClass, setupContext } from './utils.js'
|
|
|
6
6
|
import { useRestService } from '../helpers.js'
|
|
7
7
|
import { getDataSetFor } from '@furystack/repository'
|
|
8
8
|
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
9
10
|
|
|
10
11
|
describe('createDeleteEndpoint', () => {
|
|
11
12
|
it('Should delete the entity and report the success', async () => {
|
|
12
13
|
await usingAsync(new Injector(), async (i) => {
|
|
14
|
+
const port = getPort()
|
|
13
15
|
setupContext(i)
|
|
14
16
|
await useRestService<{ DELETE: { '/:id': DeleteEndpoint<MockClass, 'id'> } }>({
|
|
15
17
|
injector: i,
|
|
16
18
|
root: '/api',
|
|
17
|
-
port
|
|
19
|
+
port,
|
|
18
20
|
api: {
|
|
19
21
|
DELETE: {
|
|
20
22
|
'/:id': createDeleteEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -26,7 +28,7 @@ describe('createDeleteEndpoint', () => {
|
|
|
26
28
|
const countBeforeDelete = await getDataSetFor(i, MockClass, 'id').count(i)
|
|
27
29
|
expect(countBeforeDelete).toBe(1)
|
|
28
30
|
|
|
29
|
-
const response = await fetch(
|
|
31
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/mock`, { method: 'DELETE' })
|
|
30
32
|
expect(response.status).toBe(204)
|
|
31
33
|
const txt = await response.text()
|
|
32
34
|
expect(txt).toBe('')
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Constructable } from '@furystack/inject'
|
|
2
2
|
import type { DeleteEndpoint } from '@furystack/rest'
|
|
3
|
-
import '@furystack/repository'
|
|
4
3
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
5
4
|
import { JsonResult } from '../request-action-implementation.js'
|
|
6
5
|
import { getRepository } from '@furystack/repository'
|
|
@@ -8,6 +8,7 @@ import type { FindOptions } from '@furystack/core'
|
|
|
8
8
|
import { getDataSetFor, getRepository } from '@furystack/repository'
|
|
9
9
|
import { useRestService } from '../helpers.js'
|
|
10
10
|
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
11
12
|
|
|
12
13
|
const addMockEntities = async (i: Injector) =>
|
|
13
14
|
await getRepository(i)
|
|
@@ -24,10 +25,11 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
24
25
|
it('Should return the collection without filter / order', async () => {
|
|
25
26
|
await usingAsync(new Injector(), async (i) => {
|
|
26
27
|
setupContext(i)
|
|
28
|
+
const port = getPort()
|
|
27
29
|
await useRestService<{ GET: { '/entities': GetCollectionEndpoint<MockClass> } }>({
|
|
28
30
|
injector: i,
|
|
29
31
|
root: '/api',
|
|
30
|
-
port
|
|
32
|
+
port,
|
|
31
33
|
api: {
|
|
32
34
|
GET: {
|
|
33
35
|
'/entities': createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -39,7 +41,7 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
39
41
|
const count = await getDataSetFor(i, MockClass, 'id').count(i)
|
|
40
42
|
const allEntities = await getDataSetFor(i, MockClass, 'id').find(i, {})
|
|
41
43
|
|
|
42
|
-
const response = await fetch(
|
|
44
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/entities`, { method: 'GET' })
|
|
43
45
|
expect(response.ok).toBe(true)
|
|
44
46
|
const json: GetCollectionResult<MockClass> = await response.json()
|
|
45
47
|
expect(response.status).toBe(200)
|
|
@@ -51,10 +53,11 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
51
53
|
it('Should return entities in order', async () => {
|
|
52
54
|
await usingAsync(new Injector(), async (i) => {
|
|
53
55
|
setupContext(i)
|
|
56
|
+
const port = getPort()
|
|
54
57
|
await useRestService<{ GET: { '/entities': GetCollectionEndpoint<MockClass> } }>({
|
|
55
58
|
injector: i,
|
|
56
59
|
root: '/api',
|
|
57
|
-
port
|
|
60
|
+
port,
|
|
58
61
|
api: {
|
|
59
62
|
GET: {
|
|
60
63
|
'/entities': createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -65,7 +68,7 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
65
68
|
const findOptions: FindOptions<MockClass, Array<keyof MockClass>> = { order: { value: 'ASC' } }
|
|
66
69
|
const count = await getDataSetFor(i, MockClass, 'id').count(i, findOptions.filter)
|
|
67
70
|
const orderedEntities = await getDataSetFor(i, MockClass, 'id').find(i, findOptions)
|
|
68
|
-
const response = await fetch(`http://127.0.0.1
|
|
71
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/entities?${serializeToQueryString({ findOptions })}`, {
|
|
69
72
|
method: 'GET',
|
|
70
73
|
})
|
|
71
74
|
expect(response.ok).toBe(true)
|
|
@@ -79,10 +82,11 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
79
82
|
it('Should return entities with filtering', async () => {
|
|
80
83
|
await usingAsync(new Injector(), async (i) => {
|
|
81
84
|
setupContext(i)
|
|
85
|
+
const port = getPort()
|
|
82
86
|
await useRestService<{ GET: { '/entities': GetCollectionEndpoint<MockClass> } }>({
|
|
83
87
|
injector: i,
|
|
84
88
|
root: '/api',
|
|
85
|
-
port
|
|
89
|
+
port,
|
|
86
90
|
api: {
|
|
87
91
|
GET: {
|
|
88
92
|
'/entities': createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -99,7 +103,7 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
99
103
|
|
|
100
104
|
expect(filteredEntities).not.toContainEqual({ id: 'mock2', value: '3' })
|
|
101
105
|
|
|
102
|
-
const response = await fetch(`http://127.0.0.1
|
|
106
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/entities?${serializeToQueryString({ findOptions })}`, {
|
|
103
107
|
method: 'GET',
|
|
104
108
|
})
|
|
105
109
|
expect(response.ok).toBe(true)
|
|
@@ -113,10 +117,11 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
113
117
|
it('Should return entities with selecting specific fields', async () => {
|
|
114
118
|
await usingAsync(new Injector(), async (i) => {
|
|
115
119
|
setupContext(i)
|
|
120
|
+
const port = getPort()
|
|
116
121
|
await useRestService<{ GET: { '/entities': GetCollectionEndpoint<MockClass> } }>({
|
|
117
122
|
injector: i,
|
|
118
123
|
root: '/api',
|
|
119
|
-
port
|
|
124
|
+
port,
|
|
120
125
|
api: {
|
|
121
126
|
GET: {
|
|
122
127
|
'/entities': createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -133,7 +138,7 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
133
138
|
|
|
134
139
|
selectedEntities.forEach((e) => expect(e.value).toBeUndefined())
|
|
135
140
|
|
|
136
|
-
const response = await fetch(`http://127.0.0.1
|
|
141
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/entities?${serializeToQueryString({ findOptions })}`, {
|
|
137
142
|
method: 'GET',
|
|
138
143
|
})
|
|
139
144
|
|
|
@@ -148,10 +153,11 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
148
153
|
it('Should return entities with top/skip', async () => {
|
|
149
154
|
await usingAsync(new Injector(), async (i) => {
|
|
150
155
|
setupContext(i)
|
|
156
|
+
const port = getPort()
|
|
151
157
|
await useRestService<{ GET: { '/entities': GetCollectionEndpoint<MockClass> } }>({
|
|
152
158
|
injector: i,
|
|
153
159
|
root: '/api',
|
|
154
|
-
port
|
|
160
|
+
port,
|
|
155
161
|
api: {
|
|
156
162
|
GET: {
|
|
157
163
|
'/entities': createGetCollectionEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -170,7 +176,7 @@ describe('createGetCollectionEndpoint', () => {
|
|
|
170
176
|
expect(topSkipEntities).not.toContainEqual({ id: 'mock1', value: '4' })
|
|
171
177
|
expect(topSkipEntities).not.toContainEqual({ id: 'mock4', value: '1' })
|
|
172
178
|
|
|
173
|
-
const response = await fetch(`http://127.0.0.1
|
|
179
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/entities?${serializeToQueryString({ findOptions })}`, {
|
|
174
180
|
method: 'GET',
|
|
175
181
|
})
|
|
176
182
|
expect(response.status).toBe(200)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Constructable } from '@furystack/inject'
|
|
2
2
|
import type { GetCollectionEndpoint } from '@furystack/rest'
|
|
3
|
-
import '@furystack/repository'
|
|
4
3
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
5
4
|
import { JsonResult } from '../request-action-implementation.js'
|
|
6
5
|
import { getRepository } from '@furystack/repository'
|
|
@@ -7,15 +7,17 @@ import { createGetEntityEndpoint } from './create-get-entity-endpoint.js'
|
|
|
7
7
|
import { getDataSetFor } from '@furystack/repository'
|
|
8
8
|
import { useRestService } from '../helpers.js'
|
|
9
9
|
import { describe, it, expect } from 'vitest'
|
|
10
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
10
11
|
|
|
11
12
|
describe('createGetEntityEndpoint', () => {
|
|
12
13
|
it('Should return the entity', async () => {
|
|
13
14
|
await usingAsync(new Injector(), async (i) => {
|
|
14
15
|
setupContext(i)
|
|
16
|
+
const port = getPort()
|
|
15
17
|
await useRestService<{ GET: { '/:id': GetEntityEndpoint<MockClass, 'id'> } }>({
|
|
16
18
|
injector: i,
|
|
17
19
|
root: '/api',
|
|
18
|
-
port
|
|
20
|
+
port,
|
|
19
21
|
api: {
|
|
20
22
|
GET: {
|
|
21
23
|
'/:id': createGetEntityEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -25,7 +27,7 @@ describe('createGetEntityEndpoint', () => {
|
|
|
25
27
|
const mockEntity: MockClass = { id: 'mock', value: 'mock' }
|
|
26
28
|
await getDataSetFor(i, MockClass, 'id').add(i, mockEntity)
|
|
27
29
|
|
|
28
|
-
const response = await fetch(
|
|
30
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/mock`, { method: 'GET' })
|
|
29
31
|
expect(response.status).toBe(200)
|
|
30
32
|
const body = await response.json()
|
|
31
33
|
expect(body).toEqual(mockEntity)
|
|
@@ -35,10 +37,11 @@ describe('createGetEntityEndpoint', () => {
|
|
|
35
37
|
it('Should return the entity with the selected fields', async () => {
|
|
36
38
|
await usingAsync(new Injector(), async (i) => {
|
|
37
39
|
setupContext(i)
|
|
40
|
+
const port = getPort()
|
|
38
41
|
await useRestService<{ GET: { '/:id': GetEntityEndpoint<MockClass, 'id'> } }>({
|
|
39
42
|
injector: i,
|
|
40
43
|
root: '/api',
|
|
41
|
-
port
|
|
44
|
+
port,
|
|
42
45
|
api: {
|
|
43
46
|
GET: {
|
|
44
47
|
'/:id': createGetEntityEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -48,7 +51,7 @@ describe('createGetEntityEndpoint', () => {
|
|
|
48
51
|
const mockEntity: MockClass = { id: 'mock', value: 'mock' }
|
|
49
52
|
await getDataSetFor(i, MockClass, 'id').add(i, mockEntity)
|
|
50
53
|
|
|
51
|
-
const response = await fetch(`http://127.0.0.1
|
|
54
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/mock?${serializeToQueryString({ select: ['id'] })}`, {
|
|
52
55
|
method: 'GET',
|
|
53
56
|
})
|
|
54
57
|
expect(response.status).toBe(200)
|
|
@@ -60,17 +63,18 @@ describe('createGetEntityEndpoint', () => {
|
|
|
60
63
|
it('Should return 404 if no entity has been found', async () => {
|
|
61
64
|
await usingAsync(new Injector(), async (i) => {
|
|
62
65
|
setupContext(i)
|
|
66
|
+
const port = getPort()
|
|
63
67
|
await useRestService<{ GET: { '/:id': GetEntityEndpoint<MockClass, 'id'> } }>({
|
|
64
68
|
injector: i,
|
|
65
69
|
root: '/api',
|
|
66
|
-
port
|
|
70
|
+
port,
|
|
67
71
|
api: {
|
|
68
72
|
GET: {
|
|
69
73
|
'/:id': createGetEntityEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
70
74
|
},
|
|
71
75
|
},
|
|
72
76
|
})
|
|
73
|
-
const result = await fetch(`http://127.0.0.1
|
|
77
|
+
const result = await fetch(`http://127.0.0.1:${port}/api/mock`, { method: 'GET' })
|
|
74
78
|
expect(result.status).toBe(404)
|
|
75
79
|
const body = await result.json()
|
|
76
80
|
expect(body).toEqual({ message: 'Entity not found' })
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Constructable } from '@furystack/inject'
|
|
2
2
|
import type { GetEntityEndpoint } from '@furystack/rest'
|
|
3
3
|
import { RequestError } from '@furystack/rest'
|
|
4
|
-
import '@furystack/repository'
|
|
5
4
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
6
5
|
import { JsonResult } from '../request-action-implementation.js'
|
|
7
6
|
import { getRepository } from '@furystack/repository'
|
|
@@ -6,15 +6,17 @@ import { MockClass, setupContext } from './utils.js'
|
|
|
6
6
|
import { getDataSetFor } from '@furystack/repository'
|
|
7
7
|
import { useRestService } from '../helpers.js'
|
|
8
8
|
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
9
10
|
|
|
10
11
|
describe('createPatchEndpoint', () => {
|
|
11
12
|
it('Should update the entity and report the success', async () => {
|
|
12
13
|
await usingAsync(new Injector(), async (i) => {
|
|
13
14
|
setupContext(i)
|
|
15
|
+
const port = getPort()
|
|
14
16
|
await useRestService<{ PATCH: { '/:id': PatchEndpoint<MockClass, 'id'> } }>({
|
|
15
17
|
injector: i,
|
|
16
18
|
root: '/api',
|
|
17
|
-
port
|
|
19
|
+
port,
|
|
18
20
|
api: {
|
|
19
21
|
PATCH: {
|
|
20
22
|
'/:id': createPatchEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -26,7 +28,7 @@ describe('createPatchEndpoint', () => {
|
|
|
26
28
|
const countBeforeDelete = await getDataSetFor(i, MockClass, 'id').count(i)
|
|
27
29
|
expect(countBeforeDelete).toBe(1)
|
|
28
30
|
|
|
29
|
-
const response = await fetch(
|
|
31
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/mock`, {
|
|
30
32
|
method: 'PATCH',
|
|
31
33
|
body: JSON.stringify({ value: 'updated' }),
|
|
32
34
|
})
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Constructable } from '@furystack/inject'
|
|
2
2
|
import type { PatchEndpoint } from '@furystack/rest'
|
|
3
3
|
import '@furystack/repository'
|
|
4
|
-
import '../incoming-message-extensions.js'
|
|
5
4
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
6
5
|
import { JsonResult } from '../request-action-implementation.js'
|
|
7
6
|
import { getRepository } from '@furystack/repository'
|
|
8
7
|
import type { WithOptionalId } from '@furystack/core'
|
|
8
|
+
import { readPostBody } from '../read-post-body.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Creates a PATCH endpoint for updating entities
|
|
@@ -28,7 +28,7 @@ export const createPatchEndpoint = <
|
|
|
28
28
|
getUrlParams,
|
|
29
29
|
}) => {
|
|
30
30
|
const { id } = getUrlParams()
|
|
31
|
-
const patchData = await
|
|
31
|
+
const patchData = await readPostBody<T>(request)
|
|
32
32
|
const dataSet = getRepository(injector).getDataSetFor(options.model, options.primaryKey)
|
|
33
33
|
await dataSet.update(injector, id, patchData)
|
|
34
34
|
return JsonResult({})
|
|
@@ -6,15 +6,17 @@ import { MockClass, setupContext } from './utils.js'
|
|
|
6
6
|
import { useRestService } from '../helpers.js'
|
|
7
7
|
import { getDataSetFor } from '@furystack/repository'
|
|
8
8
|
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
9
10
|
|
|
10
11
|
describe('createPostEndpoint', () => {
|
|
11
12
|
it('Should create the entity and report the success', async () => {
|
|
12
13
|
await usingAsync(new Injector(), async (i) => {
|
|
13
14
|
setupContext(i)
|
|
15
|
+
const port = getPort()
|
|
14
16
|
await useRestService<{ POST: { '/': PostEndpoint<MockClass, 'id'> } }>({
|
|
15
17
|
injector: i,
|
|
16
18
|
root: '/api',
|
|
17
|
-
port
|
|
19
|
+
port,
|
|
18
20
|
api: {
|
|
19
21
|
POST: {
|
|
20
22
|
'/': createPostEndpoint({ model: MockClass, primaryKey: 'id' }),
|
|
@@ -22,7 +24,7 @@ describe('createPostEndpoint', () => {
|
|
|
22
24
|
},
|
|
23
25
|
})
|
|
24
26
|
const entityToPost = { id: 'mock', value: 'posted' }
|
|
25
|
-
const response = await fetch(
|
|
27
|
+
const response = await fetch(`http://127.0.0.1:${port}/api`, {
|
|
26
28
|
method: 'POST',
|
|
27
29
|
body: JSON.stringify(entityToPost),
|
|
28
30
|
})
|
|
@@ -2,11 +2,11 @@ import type { Constructable } from '@furystack/inject'
|
|
|
2
2
|
import type { PostEndpoint } from '@furystack/rest'
|
|
3
3
|
import { RequestError } from '@furystack/rest'
|
|
4
4
|
import '@furystack/repository'
|
|
5
|
-
import '../incoming-message-extensions.js'
|
|
6
5
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
7
6
|
import { JsonResult } from '../request-action-implementation.js'
|
|
8
7
|
import type { WithOptionalId } from '@furystack/core'
|
|
9
8
|
import { getRepository } from '@furystack/repository'
|
|
9
|
+
import { readPostBody } from '../read-post-body.js'
|
|
10
10
|
/**
|
|
11
11
|
* Creates a POST endpoint for updating entities
|
|
12
12
|
* @param options The options for endpoint creation
|
|
@@ -28,7 +28,7 @@ export const createPostEndpoint = <
|
|
|
28
28
|
options.primaryKey,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
const entityToCreate = await
|
|
31
|
+
const entityToCreate = await readPostBody<TWritableData>(request)
|
|
32
32
|
const { created } = await dataSet.add(injector, entityToCreate)
|
|
33
33
|
if (!created || !created.length) {
|
|
34
34
|
throw new RequestError('Entity not found', 404)
|
package/src/helpers.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const useRestService = async <T extends RestApi>(api: ImplementApiOptions
|
|
|
25
25
|
export const useHttpAuthentication = <TUser extends User, TSession extends DefaultSession>(
|
|
26
26
|
injector: Injector,
|
|
27
27
|
settings?: Partial<HttpAuthenticationSettings<TUser, TSession>>,
|
|
28
|
-
) => injector.setExplicitInstance(
|
|
28
|
+
) => injector.setExplicitInstance(Object.assign(new HttpAuthenticationSettings(), settings), HttpAuthenticationSettings)
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Sets up a static file server
|
|
@@ -11,8 +11,7 @@ import { DefaultSession } from './models/default-session.js'
|
|
|
11
11
|
export class HttpAuthenticationSettings<TUser extends User, TSession extends DefaultSession> {
|
|
12
12
|
public model: Constructable<TUser> = User as Constructable<TUser>
|
|
13
13
|
|
|
14
|
-
public getUserStore
|
|
15
|
-
sm.getStoreFor<TUser, keyof TUser>(User as any, 'username')
|
|
14
|
+
public getUserStore = (sm: StoreManager) => sm.getStoreFor(User, 'username')
|
|
16
15
|
|
|
17
16
|
public getSessionStore: (storeManager: StoreManager) => PhysicalStore<TSession, keyof TSession> = (sm) =>
|
|
18
17
|
sm.getStoreFor(DefaultSession, 'sessionId') as unknown as PhysicalStore<TSession, keyof TSession>
|
|
@@ -19,8 +19,7 @@ export const prepareInjector = async (i: Injector) => {
|
|
|
19
19
|
const setupUser = async (i: Injector, userName: string, password: string) => {
|
|
20
20
|
const sm = i.getInstance(StoreManager)
|
|
21
21
|
const pw = i.getInstance(PasswordAuthenticator)
|
|
22
|
-
const
|
|
23
|
-
const cred = await hasher.createCredential(userName, password)
|
|
22
|
+
const cred = await pw.hasher.createCredential(userName, password)
|
|
24
23
|
await sm.getStoreFor(PasswordCredential, 'userName').add(cred)
|
|
25
24
|
await sm.getStoreFor(User, 'username').add({ username: userName, roles: [] })
|
|
26
25
|
}
|
|
@@ -311,4 +310,82 @@ describe('HttpUserContext', () => {
|
|
|
311
310
|
})
|
|
312
311
|
})
|
|
313
312
|
})
|
|
313
|
+
|
|
314
|
+
describe('Changes in the store during the context lifetime', () => {
|
|
315
|
+
it('Should update user roles', () => {
|
|
316
|
+
return usingAsync(new Injector(), async (i) => {
|
|
317
|
+
await prepareInjector(i)
|
|
318
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
319
|
+
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
|
|
320
|
+
userStore.add(testUser)
|
|
321
|
+
|
|
322
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
323
|
+
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
324
|
+
|
|
325
|
+
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
326
|
+
|
|
327
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
328
|
+
expect(originalUser).toEqual(testUser)
|
|
329
|
+
|
|
330
|
+
const updatedUser = { ...testUser, roles: ['newFancyRole'] }
|
|
331
|
+
await userStore.update(testUser.username, updatedUser)
|
|
332
|
+
const updatedUserFromContext = await ctx.getCurrentUser(request)
|
|
333
|
+
expect(updatedUserFromContext.roles).toEqual(['newFancyRole'])
|
|
334
|
+
|
|
335
|
+
await userStore.update(testUser.username, { ...updatedUser, roles: [] })
|
|
336
|
+
const reloadedUserFromContext = await ctx.getCurrentUser(request)
|
|
337
|
+
expect(reloadedUserFromContext.roles).toEqual([])
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('Should remove current user when the user is removed from the store', () => {
|
|
342
|
+
return usingAsync(new Injector(), async (i) => {
|
|
343
|
+
await prepareInjector(i)
|
|
344
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
345
|
+
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
|
|
346
|
+
userStore.add(testUser)
|
|
347
|
+
|
|
348
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
349
|
+
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
350
|
+
|
|
351
|
+
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
352
|
+
|
|
353
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
354
|
+
expect(originalUser).toEqual(testUser)
|
|
355
|
+
|
|
356
|
+
await userStore.remove(testUser.username)
|
|
357
|
+
|
|
358
|
+
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('Should remove current user when the session is removed from the store', () => {
|
|
363
|
+
return usingAsync(new Injector(), async (i) => {
|
|
364
|
+
await prepareInjector(i)
|
|
365
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
366
|
+
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
|
|
367
|
+
userStore.add(testUser)
|
|
368
|
+
|
|
369
|
+
let sessionId = ''
|
|
370
|
+
|
|
371
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
372
|
+
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
373
|
+
|
|
374
|
+
await ctx.cookieLogin(testUser, {
|
|
375
|
+
setHeader: (_headerName, headerValue) => {
|
|
376
|
+
sessionId = headerValue as string
|
|
377
|
+
return {} as ServerResponse
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
382
|
+
expect(originalUser).toEqual(testUser)
|
|
383
|
+
|
|
384
|
+
const sessionStore = ctx.getSessionStore()
|
|
385
|
+
await sessionStore.remove(sessionId as string)
|
|
386
|
+
|
|
387
|
+
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
})
|
|
314
391
|
})
|
package/src/http-user-context.ts
CHANGED
|
@@ -84,7 +84,7 @@ export class HttpUserContext {
|
|
|
84
84
|
return user
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
public async getCurrentUser(request: IncomingMessage) {
|
|
87
|
+
public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>) {
|
|
88
88
|
if (!this.user) {
|
|
89
89
|
this.user = await this.authenticateRequest(request)
|
|
90
90
|
return this.user
|
|
@@ -92,7 +92,7 @@ export class HttpUserContext {
|
|
|
92
92
|
return this.user
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
public getSessionIdFromRequest(request: IncomingMessage): string | null {
|
|
95
|
+
public getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null {
|
|
96
96
|
if (request.headers.cookie) {
|
|
97
97
|
const cookies = request.headers.cookie
|
|
98
98
|
.toString()
|
|
@@ -100,7 +100,7 @@ export class HttpUserContext {
|
|
|
100
100
|
.filter((val) => val.length > 0)
|
|
101
101
|
.map((val) => {
|
|
102
102
|
const [name, value] = val.split('=')
|
|
103
|
-
return { name: name
|
|
103
|
+
return { name: name?.trim(), value: value?.trim() }
|
|
104
104
|
})
|
|
105
105
|
const sessionCookie = cookies.find((c) => c.name === this.authentication.cookieName)
|
|
106
106
|
if (sessionCookie) {
|
|
@@ -110,7 +110,7 @@ export class HttpUserContext {
|
|
|
110
110
|
return null
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
public async authenticateRequest(request: IncomingMessage): Promise<User> {
|
|
113
|
+
public async authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
|
|
114
114
|
// Basic auth
|
|
115
115
|
if (this.authentication.enableBasicAuth && request.headers.authorization) {
|
|
116
116
|
const authData = Buffer.from(request.headers.authorization.toString().split(' ')[1], 'base64')
|
|
@@ -139,7 +139,7 @@ export class HttpUserContext {
|
|
|
139
139
|
* @param serverResponse A serverResponse to set the cookie
|
|
140
140
|
* @returns the current User
|
|
141
141
|
*/
|
|
142
|
-
public async cookieLogin(user: User, serverResponse: ServerResponse): Promise<User> {
|
|
142
|
+
public async cookieLogin(user: User, serverResponse: Pick<ServerResponse, 'setHeader'>): Promise<User> {
|
|
143
143
|
const sessionId = randomBytes(32).toString('hex')
|
|
144
144
|
await this.getSessionStore().add({ sessionId, username: user.username })
|
|
145
145
|
serverResponse.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
|
|
@@ -147,10 +147,11 @@ export class HttpUserContext {
|
|
|
147
147
|
return user
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
public async cookieLogout(request: IncomingMessage, response: ServerResponse) {
|
|
150
|
+
public async cookieLogout(request: Pick<IncomingMessage, 'headers'>, response: Pick<ServerResponse, 'setHeader'>) {
|
|
151
|
+
this.user = undefined
|
|
151
152
|
const sessionId = this.getSessionIdFromRequest(request)
|
|
152
153
|
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
if (sessionId) {
|
|
155
156
|
const sessionStore = this.getSessionStore()
|
|
156
157
|
const sessions = await sessionStore.find({ filter: { sessionId: { $eq: sessionId } } })
|
|
@@ -159,11 +160,29 @@ export class HttpUserContext {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
@Injected(HttpAuthenticationSettings)
|
|
162
|
-
public readonly authentication
|
|
163
|
+
public declare readonly authentication: HttpAuthenticationSettings<User, DefaultSession>
|
|
163
164
|
|
|
164
165
|
@Injected(StoreManager)
|
|
165
|
-
private readonly storeManager
|
|
166
|
+
private declare readonly storeManager: StoreManager
|
|
166
167
|
|
|
167
168
|
@Injected(PasswordAuthenticator)
|
|
168
|
-
private readonly authenticator
|
|
169
|
+
private declare readonly authenticator: PasswordAuthenticator
|
|
170
|
+
|
|
171
|
+
public async init() {
|
|
172
|
+
this.getUserStore().addListener('onEntityUpdated', ({ id, change }) => {
|
|
173
|
+
if (this.user?.username === id) {
|
|
174
|
+
this.user = { ...this.user, ...change }
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
this.getUserStore().addListener('onEntityRemoved', ({ key }) => {
|
|
179
|
+
if (this.user?.username === key) {
|
|
180
|
+
this.user = undefined
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
this.getSessionStore().addListener('onEntityRemoved', () => {
|
|
185
|
+
this.user = undefined // as user cannot be determined by the session id anymore
|
|
186
|
+
})
|
|
187
|
+
}
|
|
169
188
|
}
|