@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.
Files changed (126) hide show
  1. package/esm/actions/login.d.ts +0 -1
  2. package/esm/actions/login.d.ts.map +1 -1
  3. package/esm/actions/login.js +0 -1
  4. package/esm/actions/login.js.map +1 -1
  5. package/esm/add-cors-header.d.ts +14 -0
  6. package/esm/add-cors-header.d.ts.map +1 -0
  7. package/esm/add-cors-header.js +23 -0
  8. package/esm/add-cors-header.js.map +1 -0
  9. package/esm/add-cors-header.spec.js +7 -8
  10. package/esm/add-cors-header.spec.js.map +1 -1
  11. package/esm/api-manager.d.ts.map +1 -1
  12. package/esm/api-manager.js +4 -5
  13. package/esm/api-manager.js.map +1 -1
  14. package/esm/endpoint-generators/create-delete-endpoint.d.ts +0 -1
  15. package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
  16. package/esm/endpoint-generators/create-delete-endpoint.js +0 -1
  17. package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
  18. package/esm/endpoint-generators/create-delete-endpoint.spec.js +4 -2
  19. package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  20. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +0 -1
  21. package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
  22. package/esm/endpoint-generators/create-get-collection-endpoint.js +0 -1
  23. package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
  24. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +16 -10
  25. package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  26. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +0 -1
  27. package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
  28. package/esm/endpoint-generators/create-get-entity-endpoint.js +0 -1
  29. package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
  30. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +10 -6
  31. package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  32. package/esm/endpoint-generators/create-patch-endpoint.d.ts +0 -1
  33. package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
  34. package/esm/endpoint-generators/create-patch-endpoint.js +2 -2
  35. package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
  36. package/esm/endpoint-generators/create-patch-endpoint.spec.js +4 -2
  37. package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  38. package/esm/endpoint-generators/create-post-endpoint.d.ts +0 -1
  39. package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
  40. package/esm/endpoint-generators/create-post-endpoint.js +2 -2
  41. package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
  42. package/esm/endpoint-generators/create-post-endpoint.spec.js +4 -2
  43. package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  44. package/esm/endpoint-generators/utils.d.ts.map +1 -1
  45. package/esm/endpoint-generators/utils.js +0 -2
  46. package/esm/endpoint-generators/utils.js.map +1 -1
  47. package/esm/helpers.d.ts.map +1 -1
  48. package/esm/helpers.js +1 -1
  49. package/esm/helpers.js.map +1 -1
  50. package/esm/http-authentication-settings.d.ts +1 -1
  51. package/esm/http-authentication-settings.d.ts.map +1 -1
  52. package/esm/http-authentication-settings.js.map +1 -1
  53. package/esm/http-user-context.d.ts +7 -6
  54. package/esm/http-user-context.d.ts.map +1 -1
  55. package/esm/http-user-context.js +17 -5
  56. package/esm/http-user-context.js.map +1 -1
  57. package/esm/http-user-context.spec.js +60 -2
  58. package/esm/http-user-context.spec.js.map +1 -1
  59. package/esm/incoming-message-extensions.d.ts +0 -2
  60. package/esm/incoming-message-extensions.d.ts.map +1 -1
  61. package/esm/incoming-message-extensions.js +1 -10
  62. package/esm/incoming-message-extensions.js.map +1 -1
  63. package/esm/index.d.ts +2 -2
  64. package/esm/index.d.ts.map +1 -1
  65. package/esm/index.js +2 -2
  66. package/esm/index.js.map +1 -1
  67. package/esm/models/default-session.d.ts.map +1 -1
  68. package/esm/models/default-session.js +0 -8
  69. package/esm/models/default-session.js.map +1 -1
  70. package/esm/read-post-body.d.ts +12 -0
  71. package/esm/read-post-body.d.ts.map +1 -0
  72. package/esm/read-post-body.js +33 -0
  73. package/esm/read-post-body.js.map +1 -0
  74. package/esm/request-action-implementation.d.ts +4 -4
  75. package/esm/request-action-implementation.d.ts.map +1 -1
  76. package/esm/rest-service.integration.spec.js +24 -33
  77. package/esm/rest-service.integration.spec.js.map +1 -1
  78. package/esm/rest.integration.test.js +1 -1
  79. package/esm/static-server-manager.d.ts.map +1 -1
  80. package/esm/static-server-manager.js +0 -1
  81. package/esm/static-server-manager.js.map +1 -1
  82. package/esm/static-server-manager.spec.js +2 -15
  83. package/esm/static-server-manager.spec.js.map +1 -1
  84. package/esm/validate.d.ts +3 -1
  85. package/esm/validate.d.ts.map +1 -1
  86. package/esm/validate.integration.spec.js +6 -2
  87. package/esm/validate.integration.spec.js.map +1 -1
  88. package/package.json +13 -13
  89. package/src/actions/login.ts +0 -1
  90. package/src/add-cors-header.spec.ts +7 -8
  91. package/src/add-cors-header.ts +32 -0
  92. package/src/api-manager.ts +5 -5
  93. package/src/endpoint-generators/create-delete-endpoint.spec.ts +4 -2
  94. package/src/endpoint-generators/create-delete-endpoint.ts +0 -1
  95. package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +16 -10
  96. package/src/endpoint-generators/create-get-collection-endpoint.ts +0 -1
  97. package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +10 -6
  98. package/src/endpoint-generators/create-get-entity-endpoint.ts +0 -1
  99. package/src/endpoint-generators/create-patch-endpoint.spec.ts +4 -2
  100. package/src/endpoint-generators/create-patch-endpoint.ts +2 -2
  101. package/src/endpoint-generators/create-post-endpoint.spec.ts +4 -2
  102. package/src/endpoint-generators/create-post-endpoint.ts +2 -2
  103. package/src/endpoint-generators/utils.ts +2 -2
  104. package/src/helpers.ts +1 -1
  105. package/src/http-authentication-settings.ts +1 -2
  106. package/src/http-user-context.spec.ts +79 -2
  107. package/src/http-user-context.ts +29 -10
  108. package/src/incoming-message-extensions.ts +0 -15
  109. package/src/index.ts +2 -2
  110. package/src/models/default-session.ts +2 -2
  111. package/src/read-post-body.ts +36 -0
  112. package/src/rest-service.integration.spec.ts +24 -38
  113. package/src/rest.integration.test.ts +1 -1
  114. package/src/static-server-manager.spec.ts +2 -18
  115. package/src/static-server-manager.ts +1 -1
  116. package/src/validate.integration.spec.ts +9 -2
  117. package/esm/incoming-message-extensions.spec.d.ts +0 -2
  118. package/esm/incoming-message-extensions.spec.d.ts.map +0 -1
  119. package/esm/incoming-message-extensions.spec.js +0 -38
  120. package/esm/incoming-message-extensions.spec.js.map +0 -1
  121. package/esm/utils.d.ts +0 -25
  122. package/esm/utils.d.ts.map +0 -1
  123. package/esm/utils.js +0 -70
  124. package/esm/utils.js.map +0 -1
  125. package/src/incoming-message-extensions.spec.ts +0 -42
  126. 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
+ }
@@ -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: () => utils.readPostBody<any>(req),
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 && options.injector.getInstance(Utils).addCorsHeaders(options.cors, options.req, options.res)
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!: 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: 1111,
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('http://127.0.0.1:1111/api/mock', { method: 'DELETE' })
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: 1112,
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('http://127.0.0.1:1112/api/entities', { method: 'GET' })
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: 1113,
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:1113/api/entities?${serializeToQueryString({ findOptions })}`, {
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: 1113,
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:1113/api/entities?${serializeToQueryString({ findOptions })}`, {
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: 1113,
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:1113/api/entities?${serializeToQueryString({ findOptions })}`, {
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: 1113,
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:1113/api/entities?${serializeToQueryString({ findOptions })}`, {
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: 1113,
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('http://127.0.0.1:1113/api/mock', { method: 'GET' })
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: 1114,
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:1114/api/mock?${serializeToQueryString({ select: ['id'] })}`, {
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: 1115,
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:1115/api/mock`, { method: 'GET' })
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: 1116,
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('http://127.0.0.1:1116/api/mock', {
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 request.readPostBody<T>()
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: 1117,
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('http://127.0.0.1:1117/api', {
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 request.readPostBody<TWritableData>()
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)
@@ -6,8 +6,8 @@ import '../helpers.js'
6
6
  import { getRepository } from '@furystack/repository'
7
7
 
8
8
  export class MockClass {
9
- id!: string
10
- value!: string
9
+ declare id: string
10
+ declare value: string
11
11
  }
12
12
 
13
13
  export const setupContext = (i: Injector) => {
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({ ...new HttpAuthenticationSettings(), ...settings }, HttpAuthenticationSettings)
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: (storeManager: StoreManager) => PhysicalStore<TUser, keyof TUser> = (sm) =>
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 hasher = pw.getHasher()
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
  })
@@ -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.trim(), value: value.trim() }
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
- this.user = undefined
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!: HttpAuthenticationSettings<User, DefaultSession>
163
+ public declare readonly authentication: HttpAuthenticationSettings<User, DefaultSession>
163
164
 
164
165
  @Injected(StoreManager)
165
- private readonly storeManager!: StoreManager
166
+ private declare readonly storeManager: StoreManager
166
167
 
167
168
  @Injected(PasswordAuthenticator)
168
- private readonly authenticator!: PasswordAuthenticator
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
  }