@owlmeans/api 0.1.18-rc.45 → 0.1.18-rc.47
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/README.md +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/api/SKILL.md +22 -20
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/service.d.ts +1 -6
- package/build/service.d.ts.map +1 -1
- package/build/service.js +6 -6
- package/build/service.js.map +1 -1
- package/build/status/consts.d.ts +5 -0
- package/build/status/consts.d.ts.map +1 -0
- package/build/status/consts.js +5 -0
- package/build/status/consts.js.map +1 -0
- package/build/status/consts.local.d.ts +10 -0
- package/build/status/consts.local.d.ts.map +1 -0
- package/build/status/consts.local.js +10 -0
- package/build/status/consts.local.js.map +1 -0
- package/build/status/helper.d.ts +4 -0
- package/build/status/helper.d.ts.map +1 -0
- package/build/status/helper.js +49 -0
- package/build/status/helper.js.map +1 -0
- package/build/status/index.d.ts +5 -27
- package/build/status/index.d.ts.map +1 -1
- package/build/status/index.js +6 -62
- package/build/status/index.js.map +1 -1
- package/build/status/marker.d.ts +1 -10
- package/build/status/marker.d.ts.map +1 -1
- package/build/status/marker.js +1 -12
- package/build/status/marker.js.map +1 -1
- package/build/status/types.d.ts +35 -0
- package/build/status/types.d.ts.map +1 -0
- package/build/status/types.js +2 -0
- package/build/status/types.js.map +1 -0
- package/build/types.local.d.ts +6 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/body/types.d.ts +36 -0
- package/build/utils/body/types.d.ts.map +1 -0
- package/build/utils/body/types.js +2 -0
- package/build/utils/body/types.js.map +1 -0
- package/build/utils/body.d.ts +3 -33
- package/build/utils/body.d.ts.map +1 -1
- package/build/utils/body.js +51 -66
- package/build/utils/body.js.map +1 -1
- package/build/utils/consts.d.ts +2 -0
- package/build/utils/consts.d.ts.map +1 -0
- package/build/utils/consts.js +2 -0
- package/build/utils/consts.js.map +1 -0
- package/build/utils/consts.local.d.ts +2 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +2 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/index.d.ts +2 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/response/types.d.ts +15 -0
- package/build/utils/response/types.d.ts.map +1 -0
- package/build/utils/response/types.js +2 -0
- package/build/utils/response/types.js.map +1 -0
- package/build/utils/response.d.ts +4 -0
- package/build/utils/response.d.ts.map +1 -0
- package/build/utils/response.js +99 -0
- package/build/utils/response.js.map +1 -0
- package/package.json +14 -14
- package/src/index.ts +3 -2
- package/src/service.ts +8 -15
- package/src/status/consts.local.ts +11 -0
- package/src/status/consts.ts +5 -0
- package/src/status/helper.ts +63 -0
- package/src/status/index.ts +7 -81
- package/src/status/marker.ts +3 -21
- package/src/status/types.ts +36 -0
- package/src/types.local.ts +7 -0
- package/src/utils/body/types.ts +37 -0
- package/src/utils/body.ts +53 -76
- package/src/utils/consts.local.ts +1 -0
- package/src/utils/consts.ts +1 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/response/types.ts +15 -0
- package/src/utils/response.ts +117 -0
- package/tests/body.spec.ts +30 -31
- package/tests/handler.spec.ts +45 -45
- package/build/utils/handler.d.ts +0 -11
- package/build/utils/handler.d.ts.map +0 -1
- package/build/utils/handler.js +0 -95
- package/build/utils/handler.js.map +0 -1
- package/src/utils/handler.ts +0 -111
package/tests/body.spec.ts
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { provideResponse } from '@owlmeans/entrypoint'
|
|
3
|
-
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
2
|
+
import { provideResponse, type AbstractRequest } from '@owlmeans/entrypoint'
|
|
4
3
|
import { RouteMethod } from '@owlmeans/route'
|
|
5
4
|
import { createApiService } from '../src/service.js'
|
|
6
|
-
import {
|
|
5
|
+
import { bodyUtils } from '../src/utils/body.js'
|
|
7
6
|
|
|
8
7
|
const STRING = { type: 'string' }
|
|
9
8
|
|
|
10
9
|
describe('what a request body becomes', () => {
|
|
11
10
|
test('a scalar on a POST without a content type is JSON text', () => {
|
|
12
|
-
expect(requestBodyOf('abc', {}, RouteMethod.POST)).toEqual({ data: '"abc"', contentType: 'application/json', verbatim: true })
|
|
13
|
-
expect(requestBodyOf('', {}, RouteMethod.POST).data).toBe('""')
|
|
14
|
-
expect(requestBodyOf(5, {}, RouteMethod.POST).data).toBe('5')
|
|
15
|
-
expect(requestBodyOf(0, {}, RouteMethod.POST).data).toBe('0')
|
|
16
|
-
expect(requestBodyOf(true, {}, RouteMethod.POST).data).toBe('true')
|
|
17
|
-
expect(requestBodyOf(false, {}, RouteMethod.POST).data).toBe('false')
|
|
11
|
+
expect(bodyUtils.requestBodyOf('abc', {}, RouteMethod.POST)).toEqual({ data: '"abc"', contentType: 'application/json', verbatim: true })
|
|
12
|
+
expect(bodyUtils.requestBodyOf('', {}, RouteMethod.POST).data).toBe('""')
|
|
13
|
+
expect(bodyUtils.requestBodyOf(5, {}, RouteMethod.POST).data).toBe('5')
|
|
14
|
+
expect(bodyUtils.requestBodyOf(0, {}, RouteMethod.POST).data).toBe('0')
|
|
15
|
+
expect(bodyUtils.requestBodyOf(true, {}, RouteMethod.POST).data).toBe('true')
|
|
16
|
+
expect(bodyUtils.requestBodyOf(false, {}, RouteMethod.POST).data).toBe('false')
|
|
18
17
|
})
|
|
19
18
|
|
|
20
19
|
test('a scalar under an explicit JSON content type is JSON text, on any method', () => {
|
|
21
|
-
expect(requestBodyOf('abc', { 'Content-Type': 'application/json; charset=utf-8' }, RouteMethod.PUT))
|
|
20
|
+
expect(bodyUtils.requestBodyOf('abc', { 'Content-Type': 'application/json; charset=utf-8' }, RouteMethod.PUT))
|
|
22
21
|
.toEqual({ data: '"abc"', verbatim: true })
|
|
23
|
-
expect(requestBodyOf(7, { 'content-type': 'application/merge-patch+json' }, RouteMethod.PATCH).data).toBe('7')
|
|
22
|
+
expect(bodyUtils.requestBodyOf(7, { 'content-type': 'application/merge-patch+json' }, RouteMethod.PATCH).data).toBe('7')
|
|
24
23
|
})
|
|
25
24
|
|
|
26
25
|
test('already-serialized JSON text is sent as it is', () => {
|
|
27
|
-
expect(requestBodyOf('{"a":1}', {}, RouteMethod.POST).data).toBe('{"a":1}')
|
|
28
|
-
expect(requestBodyOf('[1,2]', {}, RouteMethod.POST).data).toBe('[1,2]')
|
|
29
|
-
expect(requestBodyOf('"abc"', {}, RouteMethod.POST).data).toBe('"abc"')
|
|
30
|
-
expect(requestBodyOf('"abc"', {}, RouteMethod.POST, STRING).data).toBe('"abc"')
|
|
26
|
+
expect(bodyUtils.requestBodyOf('{"a":1}', {}, RouteMethod.POST).data).toBe('{"a":1}')
|
|
27
|
+
expect(bodyUtils.requestBodyOf('[1,2]', {}, RouteMethod.POST).data).toBe('[1,2]')
|
|
28
|
+
expect(bodyUtils.requestBodyOf('"abc"', {}, RouteMethod.POST).data).toBe('"abc"')
|
|
29
|
+
expect(bodyUtils.requestBodyOf('"abc"', {}, RouteMethod.POST, STRING).data).toBe('"abc"')
|
|
31
30
|
})
|
|
32
31
|
|
|
33
32
|
test('under a string body schema, JSON-looking text is the string the caller means', () => {
|
|
34
|
-
expect(jsonScalarBody('123', STRING)).toBe('"123"')
|
|
35
|
-
expect(jsonScalarBody('true', STRING)).toBe('"true"')
|
|
36
|
-
expect(jsonScalarBody('null', { type: ['string', 'null'] })).toBe('"null"')
|
|
37
|
-
expect(jsonScalarBody('{"a":1}', STRING)).toBe('"{\\"a\\":1}"')
|
|
38
|
-
expect(jsonScalarBody('123', { type: 'number' })).toBe('123')
|
|
33
|
+
expect(bodyUtils.jsonScalarBody('123', STRING)).toBe('"123"')
|
|
34
|
+
expect(bodyUtils.jsonScalarBody('true', STRING)).toBe('"true"')
|
|
35
|
+
expect(bodyUtils.jsonScalarBody('null', { type: ['string', 'null'] })).toBe('"null"')
|
|
36
|
+
expect(bodyUtils.jsonScalarBody('{"a":1}', STRING)).toBe('"{\\"a\\":1}"')
|
|
37
|
+
expect(bodyUtils.jsonScalarBody('123', { type: 'number' })).toBe('123')
|
|
39
38
|
})
|
|
40
39
|
|
|
41
40
|
test('objects, arrays and everything outside JSON behave as before', () => {
|
|
42
41
|
const object = { a: 1 }
|
|
43
|
-
expect(requestBodyOf(object, {}, RouteMethod.POST)).toEqual({ data: object, verbatim: false })
|
|
44
|
-
expect(requestBodyOf([1], {}, RouteMethod.POST)).toEqual({ data: [1], verbatim: false })
|
|
45
|
-
expect(requestBodyOf(undefined, {}, RouteMethod.POST)).toEqual({ data: undefined, verbatim: false })
|
|
46
|
-
expect(requestBodyOf('abc', { 'content-type': 'text/plain' }, RouteMethod.POST)).toEqual({ data: 'abc', verbatim: false })
|
|
47
|
-
expect(requestBodyOf('abc', {}, RouteMethod.PUT)).toEqual({ data: 'abc', verbatim: false })
|
|
48
|
-
expect(requestBodyOf({ a: '1', b: ['x'] }, { 'content-type': 'application/x-www-form-urlencoded' }, RouteMethod.POST).data)
|
|
42
|
+
expect(bodyUtils.requestBodyOf(object, {}, RouteMethod.POST)).toEqual({ data: object, verbatim: false })
|
|
43
|
+
expect(bodyUtils.requestBodyOf([1], {}, RouteMethod.POST)).toEqual({ data: [1], verbatim: false })
|
|
44
|
+
expect(bodyUtils.requestBodyOf(undefined, {}, RouteMethod.POST)).toEqual({ data: undefined, verbatim: false })
|
|
45
|
+
expect(bodyUtils.requestBodyOf('abc', { 'content-type': 'text/plain' }, RouteMethod.POST)).toEqual({ data: 'abc', verbatim: false })
|
|
46
|
+
expect(bodyUtils.requestBodyOf('abc', {}, RouteMethod.PUT)).toEqual({ data: 'abc', verbatim: false })
|
|
47
|
+
expect(bodyUtils.requestBodyOf({ a: '1', b: ['x'] }, { 'content-type': 'application/x-www-form-urlencoded' }, RouteMethod.POST).data)
|
|
49
48
|
.toBe('a=1&b%5B0%5D=x')
|
|
50
49
|
})
|
|
51
50
|
|
|
52
51
|
test('the content-type header is read whatever its case', () => {
|
|
53
|
-
expect(contentTypeOf({ 'CONTENT-TYPE': 'application/json' })).toBe('application/json')
|
|
54
|
-
expect(contentTypeOf({ accept: 'application/json' })).toBeUndefined()
|
|
55
|
-
expect(isJsonContentType('application/vnd.api+json')).toBe(true)
|
|
56
|
-
expect(isJsonContentType('application/jsonp')).toBe(false)
|
|
57
|
-
expect(isJsonContentType('text/plain')).toBe(false)
|
|
52
|
+
expect(bodyUtils.contentTypeOf({ 'CONTENT-TYPE': 'application/json' })).toBe('application/json')
|
|
53
|
+
expect(bodyUtils.contentTypeOf({ accept: 'application/json' })).toBeUndefined()
|
|
54
|
+
expect(bodyUtils.isJsonContentType('application/vnd.api+json')).toBe(true)
|
|
55
|
+
expect(bodyUtils.isJsonContentType('application/jsonp')).toBe(false)
|
|
56
|
+
expect(bodyUtils.isJsonContentType('text/plain')).toBe(false)
|
|
58
57
|
})
|
|
59
58
|
})
|
|
60
59
|
|
package/tests/handler.spec.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
|
6
6
|
import { ResilientError } from '@owlmeans/error'
|
|
7
7
|
import { RecordExists, UnknownRecordError } from '@owlmeans/resource'
|
|
8
8
|
import {
|
|
9
|
-
ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError,
|
|
9
|
+
ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError, apiStatusHelper,
|
|
10
10
|
INCIDENT_ID_HEADER,
|
|
11
11
|
DENIAL_KIND_HEADER, ACCESS_DENIED_KIND,
|
|
12
12
|
} from '../src/index.js'
|
|
13
|
-
import {
|
|
14
|
-
import * as
|
|
13
|
+
import { responseUtils } from '../src/utils/response.js'
|
|
14
|
+
import * as statusModule from '../src/status/index.js'
|
|
15
15
|
|
|
16
16
|
const INCIDENT = '6f1c1a52-8a0e-4f5e-9a7c-1f2d3e4c5b6a'
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@ const respond = (
|
|
|
41
41
|
resolve: (value, outcome) => { reply.value = value; reply.outcome = outcome },
|
|
42
42
|
reject: error => { reply.error = error },
|
|
43
43
|
}
|
|
44
|
-
processResponse({
|
|
44
|
+
responseUtils.processResponse({
|
|
45
45
|
status: statusCode, statusText: '', data, headers, config: { url: '/spec' },
|
|
46
46
|
} as unknown as AxiosResponse, reply)
|
|
47
47
|
|
|
@@ -63,64 +63,64 @@ describe('2xx', () => {
|
|
|
63
63
|
describe('a production incident body keeps its status and incident id', () => {
|
|
64
64
|
test('only a labelled 403 is an auth/IAM denial', () => {
|
|
65
65
|
const denied = respond(403, INCIDENT, { [DENIAL_KIND_HEADER]: ACCESS_DENIED_KIND }).error
|
|
66
|
-
expect(
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
66
|
+
expect(statusModule.apiStatusHelper.isAccessDenied(denied)).toBe(true)
|
|
67
|
+
expect(statusModule.apiStatusHelper.isAccessDenied(respond(403, INCIDENT).error)).toBe(false)
|
|
68
|
+
expect(statusModule.apiStatusHelper.isAccessDenied(respond(401, INCIDENT, { [DENIAL_KIND_HEADER]: ACCESS_DENIED_KIND }).error)).toBe(false)
|
|
69
69
|
})
|
|
70
70
|
test('428 with the header becomes ApiStatusError', () => {
|
|
71
71
|
const { error } = respond(428, INCIDENT, { [INCIDENT_ID_HEADER.toLowerCase()]: INCIDENT })
|
|
72
72
|
expect(error).toBeInstanceOf(ApiStatusError)
|
|
73
73
|
expect(error!.message).toBe(`api:client:status:428:${INCIDENT}`)
|
|
74
|
-
expect(httpStatusOf(error)).toBe(428)
|
|
75
|
-
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
74
|
+
expect(apiStatusHelper.httpStatusOf(error)).toBe(428)
|
|
75
|
+
expect(apiStatusHelper.incidentIdOf(error)).toBe(INCIDENT)
|
|
76
76
|
expect((error as ApiStatusError).status).toBe(428)
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
test('the header is read from AxiosHeaders too, and the body alone suffices', () => {
|
|
80
80
|
const headers = new AxiosHeaders({ [INCIDENT_ID_HEADER]: INCIDENT })
|
|
81
|
-
expect(incidentIdOf(respond(409, 'x', headers).error)).toBe(INCIDENT)
|
|
82
|
-
expect(incidentIdOf(respond(402, INCIDENT).error)).toBe(INCIDENT)
|
|
83
|
-
expect(httpStatusOf(respond(402, INCIDENT).error)).toBe(402)
|
|
81
|
+
expect(apiStatusHelper.incidentIdOf(respond(409, 'x', headers).error)).toBe(INCIDENT)
|
|
82
|
+
expect(apiStatusHelper.incidentIdOf(respond(402, INCIDENT).error)).toBe(INCIDENT)
|
|
83
|
+
expect(apiStatusHelper.httpStatusOf(respond(402, INCIDENT).error)).toBe(402)
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
test('500, 401 and 403 keep their classes and markers', () => {
|
|
87
87
|
const crashed = respond(500, INCIDENT).error
|
|
88
88
|
expect(crashed).toBeInstanceOf(ServerCrashedError)
|
|
89
89
|
expect(crashed!.message).toBe(`api:client:crashed:${INCIDENT}`)
|
|
90
|
-
expect(httpStatusOf(crashed)).toBe(500)
|
|
90
|
+
expect(apiStatusHelper.httpStatusOf(crashed)).toBe(500)
|
|
91
91
|
|
|
92
92
|
const auth = respond(401, INCIDENT).error
|
|
93
93
|
expect(auth).toBeInstanceOf(ServerAuthError)
|
|
94
94
|
expect(auth!.message).toBe(`api:client:auth:${INCIDENT}`)
|
|
95
|
-
expect(httpStatusOf(auth)).toBe(401)
|
|
95
|
+
expect(apiStatusHelper.httpStatusOf(auth)).toBe(401)
|
|
96
96
|
|
|
97
97
|
const forbidden = respond(403, INCIDENT).error
|
|
98
98
|
expect(forbidden).toBeInstanceOf(ApiClientError)
|
|
99
99
|
expect(forbidden).not.toBeInstanceOf(ApiStatusError)
|
|
100
100
|
expect(forbidden!.message).toBe(`api:client:forbidden:${INCIDENT}`)
|
|
101
|
-
expect(httpStatusOf(forbidden)).toBe(403)
|
|
101
|
+
expect(apiStatusHelper.httpStatusOf(forbidden)).toBe(403)
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
test('without an incident id the markers stay as before', () => {
|
|
105
|
-
expect(statusError(500).message).toBe('api:client:crashed:error')
|
|
106
|
-
expect(statusError(403).message).toBe('api:client:forbidden')
|
|
107
|
-
expect(incidentIdOf(statusError(500))).toBeNull()
|
|
108
|
-
expect(statusError(418).message).toBe('api:client:status:418')
|
|
105
|
+
expect(responseUtils.statusError(500).message).toBe('api:client:crashed:error')
|
|
106
|
+
expect(responseUtils.statusError(403).message).toBe('api:client:forbidden')
|
|
107
|
+
expect(apiStatusHelper.incidentIdOf(responseUtils.statusError(500))).toBeNull()
|
|
108
|
+
expect(responseUtils.statusError(418).message).toBe('api:client:status:418')
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
test('a proxy HTML page or framework JSON becomes a status error', () => {
|
|
112
112
|
const html = respond(502, '<html><body>Bad Gateway</body></html>').error
|
|
113
113
|
expect(html).toBeInstanceOf(ApiStatusError)
|
|
114
|
-
expect(httpStatusOf(html)).toBe(502)
|
|
115
|
-
expect(incidentIdOf(html)).toBeNull()
|
|
114
|
+
expect(apiStatusHelper.httpStatusOf(html)).toBe(502)
|
|
115
|
+
expect(apiStatusHelper.incidentIdOf(html)).toBeNull()
|
|
116
116
|
|
|
117
117
|
const json = respond(400, { statusCode: 400, error: 'Bad Request', message: 'body/x' }).error
|
|
118
118
|
expect(json).toBeInstanceOf(ApiStatusError)
|
|
119
|
-
expect(httpStatusOf(json)).toBe(400)
|
|
119
|
+
expect(apiStatusHelper.httpStatusOf(json)).toBe(400)
|
|
120
120
|
})
|
|
121
121
|
|
|
122
122
|
test('no class declares a static httpStatus, so a rethrow upstream stays 500', () => {
|
|
123
|
-
for (const error of [statusError(428), statusError(500), statusError(401), statusError(403)]) {
|
|
123
|
+
for (const error of [responseUtils.statusError(428), responseUtils.statusError(500), responseUtils.statusError(401), responseUtils.statusError(403)]) {
|
|
124
124
|
expect((error.constructor as { httpStatus?: unknown }).httpStatus).toBeUndefined()
|
|
125
125
|
}
|
|
126
126
|
})
|
|
@@ -133,8 +133,8 @@ describe('a development body stays its typed class', () => {
|
|
|
133
133
|
expect(error).toBeInstanceOf(ConsentRequired)
|
|
134
134
|
expect(error!.message).toBe('spec:consent-required:2')
|
|
135
135
|
expect((error as { responseStatus?: number }).responseStatus).toBe(428)
|
|
136
|
-
expect(httpStatusOf(error)).toBe(428)
|
|
137
|
-
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
136
|
+
expect(apiStatusHelper.httpStatusOf(error)).toBe(428)
|
|
137
|
+
expect(apiStatusHelper.incidentIdOf(error)).toBe(INCIDENT)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
test('a typed api error keeps its marker fields', () => {
|
|
@@ -153,8 +153,8 @@ describe('a development body stays its typed class', () => {
|
|
|
153
153
|
const { error } = respond(code, body, { [INCIDENT_ID_HEADER]: INCIDENT })
|
|
154
154
|
expect(error!.constructor).toBe(refusal.constructor)
|
|
155
155
|
expect(error!.message).toBe(refusal.message)
|
|
156
|
-
expect(httpStatusOf(error)).toBe(code)
|
|
157
|
-
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
156
|
+
expect(apiStatusHelper.httpStatusOf(error)).toBe(code)
|
|
157
|
+
expect(apiStatusHelper.incidentIdOf(error)).toBe(INCIDENT)
|
|
158
158
|
}
|
|
159
159
|
const body = ResilientError.marshal(new UnknownRecordError('shelf/3')).message
|
|
160
160
|
expect((respond(404, body).error as UnknownRecordError).id).toBe('shelf/3')
|
|
@@ -164,8 +164,8 @@ describe('a development body stays its typed class', () => {
|
|
|
164
164
|
const body = `ApiSpecUnregisteredRefusal${ResilientError.separator}spec:unregistered:x`
|
|
165
165
|
const { error } = respond(404, body, { [INCIDENT_ID_HEADER]: INCIDENT })
|
|
166
166
|
expect(error).not.toBeInstanceOf(ServerCrashedError)
|
|
167
|
-
expect(httpStatusOf(error)).toBe(404)
|
|
168
|
-
expect(incidentIdOf(error)).toBe(INCIDENT)
|
|
167
|
+
expect(apiStatusHelper.httpStatusOf(error)).toBe(404)
|
|
168
|
+
expect(apiStatusHelper.incidentIdOf(error)).toBe(INCIDENT)
|
|
169
169
|
})
|
|
170
170
|
})
|
|
171
171
|
|
|
@@ -176,7 +176,7 @@ describe('a production body of a storage refusal keeps the declared status', ()
|
|
|
176
176
|
expect(error).toBeInstanceOf(ApiStatusError)
|
|
177
177
|
expect(error).not.toBeInstanceOf(ServerCrashedError)
|
|
178
178
|
expect(error!.message).toBe(`api:client:status:${code}:${INCIDENT}`)
|
|
179
|
-
expect(httpStatusOf(error)).toBe(code)
|
|
179
|
+
expect(apiStatusHelper.httpStatusOf(error)).toBe(code)
|
|
180
180
|
}
|
|
181
181
|
})
|
|
182
182
|
})
|
|
@@ -195,35 +195,35 @@ describe('marshal round trips', () => {
|
|
|
195
195
|
const back = roundTrip(error)
|
|
196
196
|
expect(back.constructor).toBe(error.constructor)
|
|
197
197
|
expect((back as ApiClientError).status).toBe(code)
|
|
198
|
-
expect(httpStatusOf(back)).toBe(code)
|
|
199
|
-
expect(incidentIdOf(back)).toBe(INCIDENT)
|
|
198
|
+
expect(apiStatusHelper.httpStatusOf(back)).toBe(code)
|
|
199
|
+
expect(apiStatusHelper.incidentIdOf(back)).toBe(INCIDENT)
|
|
200
200
|
}
|
|
201
201
|
})
|
|
202
202
|
|
|
203
203
|
test('a legacy bare-status marker still reads', () => {
|
|
204
|
-
expect(httpStatusOf(new ApiClientError('404'))).toBe(404)
|
|
204
|
+
expect(apiStatusHelper.httpStatusOf(new ApiClientError('404'))).toBe(404)
|
|
205
205
|
})
|
|
206
206
|
})
|
|
207
207
|
|
|
208
208
|
describe('./status', () => {
|
|
209
209
|
test('a declared static 428 answers without any marker; an undeclared 5xx does not', () => {
|
|
210
|
-
expect(
|
|
211
|
-
expect(
|
|
212
|
-
expect(
|
|
213
|
-
expect(
|
|
210
|
+
expect(statusModule.apiStatusHelper.httpStatusOf(new ConsentRequired())).toBe(428)
|
|
211
|
+
expect(statusModule.apiStatusHelper.httpStatusOf(new Unavailable())).toBeNull()
|
|
212
|
+
expect(statusModule.apiStatusHelper.httpStatusOf(new Error('plain'))).toBeNull()
|
|
213
|
+
expect(statusModule.apiStatusHelper.httpStatusOf(null)).toBeNull()
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
test('reads a marker from a plain error message or a type', () => {
|
|
217
|
-
expect(
|
|
218
|
-
expect(
|
|
219
|
-
expect(
|
|
217
|
+
expect(statusModule.apiStatusHelper.httpStatusOf(new Error(`${statusModule.API_STATUS_MARKER}428:${INCIDENT}`))).toBe(428)
|
|
218
|
+
expect(statusModule.apiStatusHelper.httpStatusOf({ type: 'api:client:auth:error' })).toBe(401)
|
|
219
|
+
expect(statusModule.apiStatusHelper.incidentIdOf(new Error(`api:client:crashed:${INCIDENT}`))).toBe(INCIDENT)
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
test('isIncidentBody accepts a bare incident UUID only', () => {
|
|
223
|
-
expect(
|
|
224
|
-
expect(
|
|
225
|
-
expect(
|
|
226
|
-
expect(
|
|
227
|
-
expect(
|
|
223
|
+
expect(statusModule.apiStatusHelper.isIncidentBody(INCIDENT)).toBe(true)
|
|
224
|
+
expect(statusModule.apiStatusHelper.isIncidentBody(`${INCIDENT}\n`)).toBe(true)
|
|
225
|
+
expect(statusModule.apiStatusHelper.isIncidentBody('Unauthorized')).toBe(false)
|
|
226
|
+
expect(statusModule.apiStatusHelper.isIncidentBody(`ApiError|||api:x|||${INCIDENT}`)).toBe(false)
|
|
227
|
+
expect(statusModule.apiStatusHelper.isIncidentBody({ id: INCIDENT })).toBe(false)
|
|
228
228
|
})
|
|
229
229
|
})
|
package/build/utils/handler.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { AbstractResponse } from '@owlmeans/entrypoint';
|
|
2
|
-
import type { AxiosResponse } from 'axios';
|
|
3
|
-
import { ApiClientError } from '../errors.js';
|
|
4
|
-
export declare const processResponse: (response: AxiosResponse, reply: AbstractResponse<any>) => void;
|
|
5
|
-
/**
|
|
6
|
-
* The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
|
|
7
|
-
* `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
|
|
8
|
-
* for every other one.
|
|
9
|
-
*/
|
|
10
|
-
export declare const statusError: (status: number, incidentId?: string) => ApiClientError;
|
|
11
|
-
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAM1C,OAAO,EAAE,cAAc,EAAuD,MAAM,cAAc,CAAA;AAOlG,eAAO,MAAM,eAAe,aAAc,aAAa,SAAS,gBAAgB,CAAC,GAAG,CAAC,SAqBpF,CAAA;AAkCD;;;;GAIG;AACH,eAAO,MAAM,WAAW,WAAY,MAAM,eAAe,MAAM,KAAG,cAUjE,CAAA"}
|
package/build/utils/handler.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
2
|
-
import { ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, INCIDENT_ID_HEADER, DENIAL_KIND_HEADER, ACCESS_DENIED_KIND, OK, SERVER_ERROR, UNAUTHORIZED_ERROR, } from '../consts.js';
|
|
3
|
-
import { ResilientError } from '@owlmeans/error';
|
|
4
|
-
import { ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError } from '../errors.js';
|
|
5
|
-
import { isIncidentBody } from '../status/index.js';
|
|
6
|
-
import { logger } from '@owlmeans/log';
|
|
7
|
-
const log = logger('api');
|
|
8
|
-
export const processResponse = (response, reply) => {
|
|
9
|
-
switch (response.status) {
|
|
10
|
-
case OK:
|
|
11
|
-
reply.resolve(response.data, EntrypointOutcome.Ok);
|
|
12
|
-
return;
|
|
13
|
-
case ACCEPTED:
|
|
14
|
-
reply.resolve(response.data, EntrypointOutcome.Accepted);
|
|
15
|
-
return;
|
|
16
|
-
case CREATED:
|
|
17
|
-
return processEmptyResponse(response, reply, EntrypointOutcome.Created);
|
|
18
|
-
case FINISHED:
|
|
19
|
-
return processEmptyResponse(response, reply, EntrypointOutcome.Finished);
|
|
20
|
-
default:
|
|
21
|
-
// A refusal is ordinary traffic (401, 403, 409, 428): debug, and never the headers.
|
|
22
|
-
log.debug('Request answered with a failure', {
|
|
23
|
-
status: response.status, statusText: response.statusText, url: response.config?.url,
|
|
24
|
-
incidentId: headerOf(response.headers, INCIDENT_ID_HEADER),
|
|
25
|
-
});
|
|
26
|
-
reply.reject(failureOf(response));
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* The error a non-2xx answer rejects with — always carrying the HTTP status and, when the server
|
|
31
|
-
* sent one, its incident id.
|
|
32
|
-
*
|
|
33
|
-
* A development body is the marshaled `ResilientError`: it is rebuilt into its own class and
|
|
34
|
-
* stamped with `responseStatus`. Anything else — the bare incident id of a production body, a
|
|
35
|
-
* proxy's HTML, a framework's JSON — becomes the status error of {@link statusError}.
|
|
36
|
-
*/
|
|
37
|
-
const failureOf = (response) => {
|
|
38
|
-
const incidentId = headerOf(response.headers, INCIDENT_ID_HEADER)
|
|
39
|
-
?? (isIncidentBody(response.data) ? response.data.trim() : undefined);
|
|
40
|
-
const denialKind = response.status === FORBIDDEN_ERROR
|
|
41
|
-
&& headerOf(response.headers, DENIAL_KIND_HEADER) === ACCESS_DENIED_KIND
|
|
42
|
-
? ACCESS_DENIED_KIND : undefined;
|
|
43
|
-
if (typeof response.data === 'string' && response.data.includes(ResilientError.separator)) {
|
|
44
|
-
try {
|
|
45
|
-
const error = ResilientError.ensure(response.data, true);
|
|
46
|
-
error.incidentId ??= incidentId;
|
|
47
|
-
Object.assign(error, {
|
|
48
|
-
responseStatus: response.status, denialKind,
|
|
49
|
-
});
|
|
50
|
-
return error;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
log.warn('Server returned an unrecognizable text error', { status: response.status, body: response.data });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return Object.assign(statusError(response.status, incidentId), { denialKind });
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
|
|
60
|
-
* `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
|
|
61
|
-
* for every other one.
|
|
62
|
-
*/
|
|
63
|
-
export const statusError = (status, incidentId) => {
|
|
64
|
-
const error = status === SERVER_ERROR ? new ServerCrashedError(incidentId)
|
|
65
|
-
: status === UNAUTHORIZED_ERROR ? new ServerAuthError(incidentId)
|
|
66
|
-
: status === FORBIDDEN_ERROR ? new ApiClientError(incidentId != null ? `forbidden:${incidentId}` : 'forbidden')
|
|
67
|
-
: new ApiStatusError(status, incidentId);
|
|
68
|
-
if (incidentId != null) {
|
|
69
|
-
error.incidentId = incidentId;
|
|
70
|
-
}
|
|
71
|
-
return Object.assign(error, { responseStatus: status });
|
|
72
|
-
};
|
|
73
|
-
/** One response header, from an `AxiosHeaders` instance or a plain header object alike. */
|
|
74
|
-
const headerOf = (headers, name) => {
|
|
75
|
-
if (headers == null || typeof headers !== 'object') {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
const get = headers.get;
|
|
79
|
-
const viaGet = typeof get === 'function' ? get.call(headers, name) : undefined;
|
|
80
|
-
const value = viaGet ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
|
|
81
|
-
const first = Array.isArray(value) ? value[0] : value;
|
|
82
|
-
return typeof first === 'string' && first.trim() !== '' ? first.trim() : undefined;
|
|
83
|
-
};
|
|
84
|
-
const processEmptyResponse = (response, reply, outcome) => {
|
|
85
|
-
if (response.data != null && response.data != '') {
|
|
86
|
-
reply.resolve(response.data, outcome);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (response.headers.toJSON != null && typeof response.headers.toJSON === 'function') {
|
|
90
|
-
reply.resolve(response.headers.toJSON(), outcome);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
reply.resolve(response.headers, outcome);
|
|
94
|
-
};
|
|
95
|
-
//# sourceMappingURL=handler.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/utils/handler.ts"],"names":[],"mappings":"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"}
|
package/src/utils/handler.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { AbstractResponse } from '@owlmeans/entrypoint'
|
|
2
|
-
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
3
|
-
import type { AxiosResponse } from 'axios'
|
|
4
|
-
import {
|
|
5
|
-
ACCEPTED, CREATED, FINISHED, FORBIDDEN_ERROR, INCIDENT_ID_HEADER, DENIAL_KIND_HEADER,
|
|
6
|
-
ACCESS_DENIED_KIND, OK, SERVER_ERROR, UNAUTHORIZED_ERROR,
|
|
7
|
-
} from '../consts.js'
|
|
8
|
-
import { ResilientError } from '@owlmeans/error'
|
|
9
|
-
import { ApiClientError, ApiStatusError, ServerAuthError, ServerCrashedError } from '../errors.js'
|
|
10
|
-
import { isIncidentBody } from '../status/index.js'
|
|
11
|
-
import type { ResponseStatusCarrier } from '../status/index.js'
|
|
12
|
-
import { logger } from '@owlmeans/log'
|
|
13
|
-
|
|
14
|
-
const log = logger('api')
|
|
15
|
-
|
|
16
|
-
export const processResponse = (response: AxiosResponse, reply: AbstractResponse<any>) => {
|
|
17
|
-
switch (response.status) {
|
|
18
|
-
case OK:
|
|
19
|
-
reply.resolve(response.data, EntrypointOutcome.Ok)
|
|
20
|
-
return
|
|
21
|
-
case ACCEPTED:
|
|
22
|
-
reply.resolve(response.data, EntrypointOutcome.Accepted)
|
|
23
|
-
return
|
|
24
|
-
case CREATED:
|
|
25
|
-
return processEmptyResponse(response, reply, EntrypointOutcome.Created)
|
|
26
|
-
case FINISHED:
|
|
27
|
-
return processEmptyResponse(response, reply, EntrypointOutcome.Finished)
|
|
28
|
-
default:
|
|
29
|
-
// A refusal is ordinary traffic (401, 403, 409, 428): debug, and never the headers.
|
|
30
|
-
log.debug('Request answered with a failure', {
|
|
31
|
-
status: response.status, statusText: response.statusText, url: response.config?.url,
|
|
32
|
-
incidentId: headerOf(response.headers, INCIDENT_ID_HEADER),
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
reply.reject(failureOf(response))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The error a non-2xx answer rejects with — always carrying the HTTP status and, when the server
|
|
41
|
-
* sent one, its incident id.
|
|
42
|
-
*
|
|
43
|
-
* A development body is the marshaled `ResilientError`: it is rebuilt into its own class and
|
|
44
|
-
* stamped with `responseStatus`. Anything else — the bare incident id of a production body, a
|
|
45
|
-
* proxy's HTML, a framework's JSON — becomes the status error of {@link statusError}.
|
|
46
|
-
*/
|
|
47
|
-
const failureOf = (response: AxiosResponse): Error => {
|
|
48
|
-
const incidentId = headerOf(response.headers, INCIDENT_ID_HEADER)
|
|
49
|
-
?? (isIncidentBody(response.data) ? response.data.trim() : undefined)
|
|
50
|
-
const denialKind = response.status === FORBIDDEN_ERROR
|
|
51
|
-
&& headerOf(response.headers, DENIAL_KIND_HEADER) === ACCESS_DENIED_KIND
|
|
52
|
-
? ACCESS_DENIED_KIND : undefined
|
|
53
|
-
|
|
54
|
-
if (typeof response.data === 'string' && response.data.includes(ResilientError.separator)) {
|
|
55
|
-
try {
|
|
56
|
-
const error = ResilientError.ensure(response.data, true)
|
|
57
|
-
error.incidentId ??= incidentId
|
|
58
|
-
Object.assign(error, {
|
|
59
|
-
responseStatus: response.status, denialKind,
|
|
60
|
-
} satisfies ResponseStatusCarrier)
|
|
61
|
-
|
|
62
|
-
return error
|
|
63
|
-
} catch {
|
|
64
|
-
log.warn('Server returned an unrecognizable text error', { status: response.status, body: response.data })
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return Object.assign(statusError(response.status, incidentId), { denialKind })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The typed error of a bare status: `ServerCrashedError` (500), `ServerAuthError` (401),
|
|
73
|
-
* `ApiClientError('forbidden[:<id>]')` (403), `ApiStatusError` (`api:client:status:<n>[:<id>]`)
|
|
74
|
-
* for every other one.
|
|
75
|
-
*/
|
|
76
|
-
export const statusError = (status: number, incidentId?: string): ApiClientError => {
|
|
77
|
-
const error = status === SERVER_ERROR ? new ServerCrashedError(incidentId)
|
|
78
|
-
: status === UNAUTHORIZED_ERROR ? new ServerAuthError(incidentId)
|
|
79
|
-
: status === FORBIDDEN_ERROR ? new ApiClientError(incidentId != null ? `forbidden:${incidentId}` : 'forbidden')
|
|
80
|
-
: new ApiStatusError(status, incidentId)
|
|
81
|
-
if (incidentId != null) {
|
|
82
|
-
error.incidentId = incidentId
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return Object.assign(error, { responseStatus: status } satisfies ResponseStatusCarrier)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** One response header, from an `AxiosHeaders` instance or a plain header object alike. */
|
|
89
|
-
const headerOf = (headers: unknown, name: string): string | undefined => {
|
|
90
|
-
if (headers == null || typeof headers !== 'object') {
|
|
91
|
-
return undefined
|
|
92
|
-
}
|
|
93
|
-
const get = (headers as { get?: unknown }).get
|
|
94
|
-
const viaGet = typeof get === 'function' ? get.call(headers, name) : undefined
|
|
95
|
-
const value = viaGet ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]
|
|
96
|
-
const first = Array.isArray(value) ? value[0] : value
|
|
97
|
-
|
|
98
|
-
return typeof first === 'string' && first.trim() !== '' ? first.trim() : undefined
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const processEmptyResponse = (response: AxiosResponse, reply: AbstractResponse<any>, outcome: EntrypointOutcome) => {
|
|
102
|
-
if (response.data != null && response.data != '') {
|
|
103
|
-
reply.resolve(response.data, outcome)
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
if (response.headers.toJSON != null && typeof response.headers.toJSON === 'function') {
|
|
107
|
-
reply.resolve(response.headers.toJSON(), outcome)
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
reply.resolve(response.headers, outcome)
|
|
111
|
-
}
|