@furystack/rest-service 4.0.19
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/LICENSE +339 -0
- package/README.md +219 -0
- package/dist/actions/error-action.d.ts +14 -0
- package/dist/actions/error-action.d.ts.map +1 -0
- package/dist/actions/error-action.js +29 -0
- package/dist/actions/error-action.js.map +1 -0
- package/dist/actions/error-action.spec.d.ts +2 -0
- package/dist/actions/error-action.spec.d.ts.map +1 -0
- package/dist/actions/error-action.spec.js +51 -0
- package/dist/actions/error-action.spec.js.map +1 -0
- package/dist/actions/get-current-user.d.ts +11 -0
- package/dist/actions/get-current-user.d.ts.map +1 -0
- package/dist/actions/get-current-user.js +15 -0
- package/dist/actions/get-current-user.js.map +1 -0
- package/dist/actions/get-current-user.spec.d.ts +2 -0
- package/dist/actions/get-current-user.spec.d.ts.map +1 -0
- package/dist/actions/get-current-user.spec.js +20 -0
- package/dist/actions/get-current-user.spec.js.map +1 -0
- package/dist/actions/index.d.ts +7 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/is-authenticated.d.ts +14 -0
- package/dist/actions/is-authenticated.d.ts.map +1 -0
- package/dist/actions/is-authenticated.js +17 -0
- package/dist/actions/is-authenticated.js.map +1 -0
- package/dist/actions/is-authenticated.spec.d.ts +2 -0
- package/dist/actions/is-authenticated.spec.d.ts.map +1 -0
- package/dist/actions/is-authenticated.spec.js +19 -0
- package/dist/actions/is-authenticated.spec.js.map +1 -0
- package/dist/actions/login-action.spec.d.ts +2 -0
- package/dist/actions/login-action.spec.d.ts.map +1 -0
- package/dist/actions/login-action.spec.js +35 -0
- package/dist/actions/login-action.spec.js.map +1 -0
- package/dist/actions/login.d.ts +16 -0
- package/dist/actions/login.d.ts.map +1 -0
- package/dist/actions/login.js +26 -0
- package/dist/actions/login.js.map +1 -0
- package/dist/actions/logout-action.spec.d.ts +2 -0
- package/dist/actions/logout-action.spec.d.ts.map +1 -0
- package/dist/actions/logout-action.spec.js +23 -0
- package/dist/actions/logout-action.spec.js.map +1 -0
- package/dist/actions/logout.d.ts +14 -0
- package/dist/actions/logout.d.ts.map +1 -0
- package/dist/actions/logout.js +20 -0
- package/dist/actions/logout.js.map +1 -0
- package/dist/actions/not-found-action.d.ts +10 -0
- package/dist/actions/not-found-action.d.ts.map +1 -0
- package/dist/actions/not-found-action.js +14 -0
- package/dist/actions/not-found-action.js.map +1 -0
- package/dist/actions/not-found-action.spec.d.ts +2 -0
- package/dist/actions/not-found-action.spec.d.ts.map +1 -0
- package/dist/actions/not-found-action.spec.js +17 -0
- package/dist/actions/not-found-action.spec.js.map +1 -0
- package/dist/add-cors-header.spec.d.ts +2 -0
- package/dist/add-cors-header.spec.d.ts.map +1 -0
- package/dist/add-cors-header.spec.js +99 -0
- package/dist/add-cors-header.spec.js.map +1 -0
- package/dist/api-manager.d.ts +61 -0
- package/dist/api-manager.d.ts.map +1 -0
- package/dist/api-manager.js +144 -0
- package/dist/api-manager.js.map +1 -0
- package/dist/authenticate.d.ts +5 -0
- package/dist/authenticate.d.ts.map +1 -0
- package/dist/authenticate.js +20 -0
- package/dist/authenticate.js.map +1 -0
- package/dist/authenticate.spec.d.ts +2 -0
- package/dist/authenticate.spec.d.ts.map +1 -0
- package/dist/authenticate.spec.js +59 -0
- package/dist/authenticate.spec.js.map +1 -0
- package/dist/authorize.d.ts +5 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +22 -0
- package/dist/authorize.js.map +1 -0
- package/dist/authorize.spec.d.ts +2 -0
- package/dist/authorize.spec.d.ts.map +1 -0
- package/dist/authorize.spec.js +55 -0
- package/dist/authorize.spec.js.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-delete-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.js +24 -0
- package/dist/endpoint-generators/create-delete-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.js +33 -0
- package/dist/endpoint-generators/create-delete-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.js +26 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js +143 -0
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.d.ts +17 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.js +29 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js +74 -0
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.d.ts +18 -0
- package/dist/endpoint-generators/create-patch-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.js +26 -0
- package/dist/endpoint-generators/create-patch-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.js +36 -0
- package/dist/endpoint-generators/create-patch-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.d.ts +18 -0
- package/dist/endpoint-generators/create-post-endpoint.d.ts.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.js +29 -0
- package/dist/endpoint-generators/create-post-endpoint.js.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.d.ts +2 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.d.ts.map +1 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.js +34 -0
- package/dist/endpoint-generators/create-post-endpoint.spec.js.map +1 -0
- package/dist/endpoint-generators/index.d.ts +6 -0
- package/dist/endpoint-generators/index.d.ts.map +1 -0
- package/dist/endpoint-generators/index.js +9 -0
- package/dist/endpoint-generators/index.js.map +1 -0
- package/dist/endpoint-generators/utils.d.ts +9 -0
- package/dist/endpoint-generators/utils.d.ts.map +1 -0
- package/dist/endpoint-generators/utils.js +27 -0
- package/dist/endpoint-generators/utils.js.map +1 -0
- package/dist/http-authentication-settings.d.ts +17 -0
- package/dist/http-authentication-settings.d.ts.map +1 -0
- package/dist/http-authentication-settings.js +26 -0
- package/dist/http-authentication-settings.js.map +1 -0
- package/dist/http-user-context.d.ts +54 -0
- package/dist/http-user-context.d.ts.map +1 -0
- package/dist/http-user-context.js +153 -0
- package/dist/http-user-context.js.map +1 -0
- package/dist/http-user-context.spec.d.ts +4 -0
- package/dist/http-user-context.spec.d.ts.map +1 -0
- package/dist/http-user-context.spec.js +267 -0
- package/dist/http-user-context.spec.js.map +1 -0
- package/dist/incoming-message-extensions.d.ts +8 -0
- package/dist/incoming-message-extensions.d.ts.map +1 -0
- package/dist/incoming-message-extensions.js +14 -0
- package/dist/incoming-message-extensions.js.map +1 -0
- package/dist/incoming-message-extensions.spec.d.ts +2 -0
- package/dist/incoming-message-extensions.spec.d.ts.map +1 -0
- package/dist/incoming-message-extensions.spec.js +39 -0
- package/dist/incoming-message-extensions.spec.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/injector-extensions.d.ts +21 -0
- package/dist/injector-extensions.d.ts.map +1 -0
- package/dist/injector-extensions.js +14 -0
- package/dist/injector-extensions.js.map +1 -0
- package/dist/injector-extensions.spec.d.ts +2 -0
- package/dist/injector-extensions.spec.d.ts.map +1 -0
- package/dist/injector-extensions.spec.js +19 -0
- package/dist/injector-extensions.spec.js.map +1 -0
- package/dist/models/cors-options.d.ts +22 -0
- package/dist/models/cors-options.d.ts.map +1 -0
- package/dist/models/cors-options.js +3 -0
- package/dist/models/cors-options.js.map +1 -0
- package/dist/models/default-session.d.ts +14 -0
- package/dist/models/default-session.d.ts.map +1 -0
- package/dist/models/default-session.js +10 -0
- package/dist/models/default-session.js.map +1 -0
- package/dist/request-action-implementation.d.ts +54 -0
- package/dist/request-action-implementation.d.ts.map +1 -0
- package/dist/request-action-implementation.js +42 -0
- package/dist/request-action-implementation.js.map +1 -0
- package/dist/rest-service.integration.spec.d.ts +2 -0
- package/dist/rest-service.integration.spec.d.ts.map +1 -0
- package/dist/rest-service.integration.spec.js +129 -0
- package/dist/rest-service.integration.spec.js.map +1 -0
- package/dist/rest.integration.test.d.ts +58 -0
- package/dist/rest.integration.test.d.ts.map +1 -0
- package/dist/rest.integration.test.js +94 -0
- package/dist/rest.integration.test.js.map +1 -0
- package/dist/schema-validator/index.d.ts +3 -0
- package/dist/schema-validator/index.d.ts.map +1 -0
- package/dist/schema-validator/index.js +6 -0
- package/dist/schema-validator/index.js.map +1 -0
- package/dist/schema-validator/schema-validation-error.d.ts +10 -0
- package/dist/schema-validator/schema-validation-error.d.ts.map +1 -0
- package/dist/schema-validator/schema-validation-error.js +15 -0
- package/dist/schema-validator/schema-validation-error.js.map +1 -0
- package/dist/schema-validator/schema-validator.d.ts +20 -0
- package/dist/schema-validator/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator/schema-validator.js +36 -0
- package/dist/schema-validator/schema-validator.js.map +1 -0
- package/dist/schema-validator/schema-validator.test.d.ts +2 -0
- package/dist/schema-validator/schema-validator.test.d.ts.map +1 -0
- package/dist/schema-validator/schema-validator.test.js +62 -0
- package/dist/schema-validator/schema-validator.test.js.map +1 -0
- package/dist/schema-validator/validate-examples.d.ts +37 -0
- package/dist/schema-validator/validate-examples.d.ts.map +1 -0
- package/dist/schema-validator/validate-examples.js +29 -0
- package/dist/schema-validator/validate-examples.js.map +1 -0
- package/dist/server-manager.d.ts +30 -0
- package/dist/server-manager.d.ts.map +1 -0
- package/dist/server-manager.js +71 -0
- package/dist/server-manager.js.map +1 -0
- package/dist/server-response-extensions.d.ts +21 -0
- package/dist/server-response-extensions.d.ts.map +1 -0
- package/dist/server-response-extensions.js +15 -0
- package/dist/server-response-extensions.js.map +1 -0
- package/dist/server-response-extensions.spec.d.ts +2 -0
- package/dist/server-response-extensions.spec.d.ts.map +1 -0
- package/dist/server-response-extensions.spec.js +49 -0
- package/dist/server-response-extensions.spec.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +66 -0
- package/dist/utils.js.map +1 -0
- package/dist/validate.d.ts +18 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.integration.schema.d.ts +69 -0
- package/dist/validate.integration.schema.d.ts.map +1 -0
- package/dist/validate.integration.schema.js +3 -0
- package/dist/validate.integration.schema.js.map +1 -0
- package/dist/validate.integration.spec.d.ts +13 -0
- package/dist/validate.integration.spec.d.ts.map +1 -0
- package/dist/validate.integration.spec.js +223 -0
- package/dist/validate.integration.spec.js.map +1 -0
- package/dist/validate.integration.spec.schema.json +749 -0
- package/dist/validate.js +49 -0
- package/dist/validate.js.map +1 -0
- package/package.json +56 -0
- package/src/actions/error-action.spec.ts +54 -0
- package/src/actions/error-action.ts +34 -0
- package/src/actions/get-current-user.spec.ts +23 -0
- package/src/actions/get-current-user.ts +15 -0
- package/src/actions/index.ts +6 -0
- package/src/actions/is-authenticated.spec.ts +18 -0
- package/src/actions/is-authenticated.ts +13 -0
- package/src/actions/login-action.spec.ts +41 -0
- package/src/actions/login.ts +26 -0
- package/src/actions/logout-action.spec.ts +27 -0
- package/src/actions/logout.ts +16 -0
- package/src/actions/not-found-action.spec.ts +17 -0
- package/src/actions/not-found-action.ts +13 -0
- package/src/add-cors-header.spec.ts +133 -0
- package/src/api-manager.ts +222 -0
- package/src/authenticate.spec.ts +78 -0
- package/src/authenticate.ts +22 -0
- package/src/authorize.spec.ts +69 -0
- package/src/authorize.ts +19 -0
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +34 -0
- package/src/endpoint-generators/create-delete-endpoint.ts +25 -0
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +164 -0
- package/src/endpoint-generators/create-get-collection-endpoint.ts +28 -0
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +75 -0
- package/src/endpoint-generators/create-get-entity-endpoint.ts +29 -0
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +36 -0
- package/src/endpoint-generators/create-patch-endpoint.ts +27 -0
- package/src/endpoint-generators/create-post-endpoint.spec.ts +32 -0
- package/src/endpoint-generators/create-post-endpoint.ts +30 -0
- package/src/endpoint-generators/index.ts +5 -0
- package/src/endpoint-generators/utils.ts +34 -0
- package/src/http-authentication-settings.ts +23 -0
- package/src/http-user-context.spec.ts +299 -0
- package/src/http-user-context.ts +160 -0
- package/src/incoming-message-extensions.spec.ts +41 -0
- package/src/incoming-message-extensions.ts +19 -0
- package/src/index.ts +16 -0
- package/src/injector-extensions.spec.ts +19 -0
- package/src/injector-extensions.ts +35 -0
- package/src/models/cors-options.ts +21 -0
- package/src/models/default-session.ts +14 -0
- package/src/request-action-implementation.ts +70 -0
- package/src/rest-service.integration.spec.ts +166 -0
- package/src/rest.integration.test.ts +112 -0
- package/src/schema-validator/index.ts +2 -0
- package/src/schema-validator/schema-validation-error.ts +11 -0
- package/src/schema-validator/schema-validator.test.ts +72 -0
- package/src/schema-validator/schema-validator.ts +31 -0
- package/src/schema-validator/validate-examples.ts +38 -0
- package/src/server-manager.ts +88 -0
- package/src/server-response-extensions.spec.ts +53 -0
- package/src/server-response-extensions.ts +30 -0
- package/src/utils.ts +65 -0
- package/src/validate.integration.schema.ts +50 -0
- package/src/validate.integration.spec.schema.json +779 -0
- package/src/validate.integration.spec.ts +218 -0
- package/src/validate.ts +60 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RequestError } from '@furystack/rest'
|
|
2
|
+
import { ErrorObject } from 'ajv'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom Error class for Schema Validation Errors
|
|
6
|
+
*/
|
|
7
|
+
export class SchemaValidationError extends RequestError {
|
|
8
|
+
constructor(public readonly errors: ErrorObject[]) {
|
|
9
|
+
super('Schema Validation failed', 400)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SchemaValidator } from './schema-validator'
|
|
2
|
+
import { exampleSchema, BodyParameters, Language } from './validate-examples'
|
|
3
|
+
import { SchemaValidationError } from './schema-validation-error'
|
|
4
|
+
|
|
5
|
+
describe('ValidateSchema', () => {
|
|
6
|
+
describe('String Literal checks', () => {
|
|
7
|
+
it('Should pass on valid string literal parameters', () => {
|
|
8
|
+
expect(new SchemaValidator(exampleSchema).isValid<Language>('en', { schemaName: 'Language' })).toBeTruthy()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('Should throw a ValidationError on Age parameters', () => {
|
|
12
|
+
try {
|
|
13
|
+
const result = new SchemaValidator(exampleSchema).isValid<Language>('foo', { schemaName: 'Language' })
|
|
14
|
+
expect(result).toBeFalsy() // should not hit
|
|
15
|
+
} catch (error) {
|
|
16
|
+
expect(error).toBeInstanceOf(SchemaValidationError)
|
|
17
|
+
const { errors } = error as SchemaValidationError
|
|
18
|
+
expect(errors).toHaveLength(1)
|
|
19
|
+
expect(errors[0].message).toEqual('must be equal to one of the allowed values')
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('Object checks', () => {
|
|
25
|
+
it('Should fail when string is passed instead of object', () => {
|
|
26
|
+
try {
|
|
27
|
+
new SchemaValidator(exampleSchema).isValid<BodyParameters>('foo', { schemaName: 'BodyParameters' })
|
|
28
|
+
} catch (error) {
|
|
29
|
+
expect(error).toBeInstanceOf(SchemaValidationError)
|
|
30
|
+
const { errors } = error as SchemaValidationError
|
|
31
|
+
expect(errors).toHaveLength(1)
|
|
32
|
+
expect(errors[0].message).toEqual('must be object')
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Should fail when an additional property is present', () => {
|
|
37
|
+
try {
|
|
38
|
+
new SchemaValidator(exampleSchema).isValid<BodyParameters>(
|
|
39
|
+
{ age: '3', foo: 2 },
|
|
40
|
+
{ schemaName: 'BodyParameters' },
|
|
41
|
+
)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
expect(error).toBeInstanceOf(SchemaValidationError)
|
|
44
|
+
const { errors } = error as SchemaValidationError
|
|
45
|
+
expect(errors).toHaveLength(1)
|
|
46
|
+
expect(errors[0].message).toEqual('must NOT have additional properties')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('Should pass with empty objects', () => {
|
|
51
|
+
expect(
|
|
52
|
+
new SchemaValidator(exampleSchema).isValid<BodyParameters>({}, { schemaName: 'BodyParameters' }),
|
|
53
|
+
).toBeTruthy()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('Should pass with valid partial objects', () => {
|
|
57
|
+
expect(
|
|
58
|
+
new SchemaValidator(exampleSchema).isValid<BodyParameters>({ age: '3' }, { schemaName: 'BodyParameters' }),
|
|
59
|
+
).toBeTruthy()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('Multiple checks per validator instance', () => {
|
|
64
|
+
it('Should pass with multiple types from schema per validator instance', () => {
|
|
65
|
+
const validator = new SchemaValidator(exampleSchema)
|
|
66
|
+
expect(validator.isValid<Language>('en', { schemaName: 'Language' })).toBeTruthy()
|
|
67
|
+
expect(
|
|
68
|
+
new SchemaValidator(exampleSchema).isValid<BodyParameters>({}, { schemaName: 'BodyParameters' }),
|
|
69
|
+
).toBeTruthy()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Ajv, { ErrorObject, Options } from 'ajv'
|
|
2
|
+
import useFormats from 'ajv-formats'
|
|
3
|
+
import { SchemaValidationError } from './schema-validation-error'
|
|
4
|
+
|
|
5
|
+
export class SchemaValidator<TSchema extends { definitions: {} }> {
|
|
6
|
+
private readonly ajv = new Ajv({
|
|
7
|
+
allErrors: true,
|
|
8
|
+
...this.ajvOptions,
|
|
9
|
+
})
|
|
10
|
+
constructor(private readonly schema: TSchema, private readonly ajvOptions?: Options) {
|
|
11
|
+
useFormats(this.ajv)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param data The object to validate
|
|
16
|
+
* @param options Options for the schema validation
|
|
17
|
+
* @param options.schemaName The name of the type in the Schema Definitions
|
|
18
|
+
* @throws SchemaValidationError when the validation has been failed
|
|
19
|
+
* @returns true in case of validation success
|
|
20
|
+
*/
|
|
21
|
+
public isValid<T>(data: any, options: { schemaName: keyof TSchema['definitions'] }): data is T {
|
|
22
|
+
const schema = { ...this.schema, $ref: `#/definitions/${options.schemaName}` }
|
|
23
|
+
const validatorFn = this.ajv.compile(schema)
|
|
24
|
+
const isValid = validatorFn(data)
|
|
25
|
+
if (!isValid) {
|
|
26
|
+
throw new SchemaValidationError(validatorFn.errors as ErrorObject[])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* example string literal type - needed for Typescipt Generics
|
|
3
|
+
*/
|
|
4
|
+
export type Language = 'en' | 'hu' | 'de' | 'all'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* example object type - needed for TypeScipt Generics
|
|
8
|
+
*/
|
|
9
|
+
export interface BodyParameters {
|
|
10
|
+
gender?: 'MALE' | 'FEMALE'
|
|
11
|
+
age?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Example Schema Definition - can be generated from the example types above with ts-json-schema-generator
|
|
16
|
+
*/
|
|
17
|
+
export const exampleSchema = {
|
|
18
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
19
|
+
definitions: {
|
|
20
|
+
BodyParameters: {
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
age: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
},
|
|
26
|
+
gender: {
|
|
27
|
+
enum: ['MALE', 'FEMALE'],
|
|
28
|
+
type: 'string',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
type: 'object',
|
|
32
|
+
},
|
|
33
|
+
Language: {
|
|
34
|
+
enum: ['en', 'hu', 'de', 'all'],
|
|
35
|
+
type: 'string',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Injectable } from '@furystack/inject'
|
|
2
|
+
import { Disposable } from '@furystack/utils'
|
|
3
|
+
import { Server, createServer } from 'http'
|
|
4
|
+
import Semaphore from 'semaphore-async-await'
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
6
|
+
import { Socket } from 'net'
|
|
7
|
+
|
|
8
|
+
export interface ServerOptions {
|
|
9
|
+
hostName?: string
|
|
10
|
+
port: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OnRequest {
|
|
14
|
+
req: IncomingMessage
|
|
15
|
+
res: ServerResponse
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServerRecord {
|
|
19
|
+
server: Server
|
|
20
|
+
apis: Array<{ shouldExec: (options: OnRequest) => boolean; onRequest: (options: OnRequest) => void }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Injectable({ lifetime: 'singleton' })
|
|
24
|
+
export class ServerManager implements Disposable {
|
|
25
|
+
public static DEFAULT_HOST = 'localhost'
|
|
26
|
+
|
|
27
|
+
public servers = new Map<string, ServerRecord>()
|
|
28
|
+
private openedSockets = new Set<Socket>()
|
|
29
|
+
|
|
30
|
+
private readonly listenLock = new Semaphore(1)
|
|
31
|
+
|
|
32
|
+
private getHostUrl = (options: ServerOptions) =>
|
|
33
|
+
`http://${options.hostName || ServerManager.DEFAULT_HOST}:${options.port}`
|
|
34
|
+
|
|
35
|
+
private onConnection = (socket: Socket) => {
|
|
36
|
+
this.openedSockets.add(socket)
|
|
37
|
+
socket.once('close', () => this.openedSockets.delete(socket))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async dispose() {
|
|
41
|
+
try {
|
|
42
|
+
await this.listenLock.waitFor(5000)
|
|
43
|
+
} finally {
|
|
44
|
+
this.openedSockets.forEach((s) => s.destroy())
|
|
45
|
+
await Promise.allSettled(
|
|
46
|
+
[...this.servers.values()].map(
|
|
47
|
+
(s) =>
|
|
48
|
+
new Promise<void>((resolve, reject) => {
|
|
49
|
+
s.server.close((err) => (err ? reject(err) : resolve()))
|
|
50
|
+
s.server.off('connection', this.onConnection)
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
this.servers.clear()
|
|
55
|
+
this.listenLock.release()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public async getOrCreate(options: ServerOptions): Promise<ServerRecord> {
|
|
60
|
+
const url = this.getHostUrl(options)
|
|
61
|
+
if (!this.servers.has(url)) {
|
|
62
|
+
await this.listenLock.acquire()
|
|
63
|
+
try {
|
|
64
|
+
if (!this.servers.has(url)) {
|
|
65
|
+
await new Promise<void>((resolve, reject) => {
|
|
66
|
+
const apis: ServerRecord['apis'] = []
|
|
67
|
+
const server = createServer((req, res) => {
|
|
68
|
+
const apiMatch = apis.find((api) => api.shouldExec({ req, res }))
|
|
69
|
+
if (apiMatch) {
|
|
70
|
+
apiMatch.onRequest({ req, res })
|
|
71
|
+
} else {
|
|
72
|
+
res.destroy()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
server.on('connection', this.onConnection)
|
|
76
|
+
server.on('listening', () => resolve())
|
|
77
|
+
server.on('error', (err) => reject(err))
|
|
78
|
+
server.listen(options.port, options.hostName)
|
|
79
|
+
this.servers.set(url, { server, apis })
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
this.listenLock.release()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return this.servers.get(url) as ServerRecord
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Socket } from 'net'
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
3
|
+
import './server-response-extensions'
|
|
4
|
+
import { BypassResult, JsonResult, PlainTextResult } from './request-action-implementation'
|
|
5
|
+
|
|
6
|
+
describe('ServerResponse extensions', () => {
|
|
7
|
+
describe('sendActionResult', () => {
|
|
8
|
+
it('Should be extended', () => {
|
|
9
|
+
const socket = new Socket()
|
|
10
|
+
const msg = new ServerResponse(new IncomingMessage(socket))
|
|
11
|
+
expect(typeof msg.sendActionResult).toBe('function')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('Should send the JSON response with the correct Content Type header and default status', (done) => {
|
|
15
|
+
const jsonValue = { value: Math.random() }
|
|
16
|
+
const socket = new Socket()
|
|
17
|
+
const msg = new ServerResponse(new IncomingMessage(socket))
|
|
18
|
+
msg.writeHead = jest.fn()
|
|
19
|
+
msg.end = (chunk?: any) => {
|
|
20
|
+
expect(chunk).toBe(JSON.stringify(jsonValue))
|
|
21
|
+
expect(msg.writeHead).toBeCalledWith(200, { 'Content-Type': 'application/json' })
|
|
22
|
+
done()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
msg.sendActionResult(JsonResult(jsonValue))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('Should send the plain TEXT response with the correct Content Type header and default status', (done) => {
|
|
29
|
+
const textValue = `${Math.random()}`
|
|
30
|
+
const socket = new Socket()
|
|
31
|
+
const msg = new ServerResponse(new IncomingMessage(socket))
|
|
32
|
+
msg.writeHead = jest.fn()
|
|
33
|
+
msg.end = (chunk?: any) => {
|
|
34
|
+
expect(chunk).toBe(textValue)
|
|
35
|
+
expect(msg.writeHead).toBeCalledWith(200, { 'Content-Type': 'plain/text' })
|
|
36
|
+
done()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
msg.sendActionResult(PlainTextResult(textValue))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('Should skip sending on BypassResult', () => {
|
|
43
|
+
const socket = new Socket()
|
|
44
|
+
const msg = new ServerResponse(new IncomingMessage(socket))
|
|
45
|
+
msg.writeHead = jest.fn()
|
|
46
|
+
msg.end = jest.fn()
|
|
47
|
+
|
|
48
|
+
msg.sendActionResult(BypassResult())
|
|
49
|
+
expect(msg.writeHead).not.toBeCalled()
|
|
50
|
+
expect(msg.end).not.toBeCalled()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import { ActionResult } from './request-action-implementation'
|
|
3
|
+
|
|
4
|
+
export interface SendJsonOptions<T> {
|
|
5
|
+
statusCode?: number
|
|
6
|
+
json: T
|
|
7
|
+
headers?: { [K: string]: string }
|
|
8
|
+
}
|
|
9
|
+
export interface SendPlainTextOptions {
|
|
10
|
+
statusCode?: number
|
|
11
|
+
text: string
|
|
12
|
+
headers?: { [K: string]: string }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module 'http' {
|
|
16
|
+
export interface ServerResponse {
|
|
17
|
+
sendActionResult: <T>(result: ActionResult<T>) => void
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
http.ServerResponse.prototype.sendActionResult = function <T>(options: ActionResult<T>) {
|
|
22
|
+
if (typeof options.chunk === 'object') {
|
|
23
|
+
options.chunk = JSON.stringify(options.chunk) as any
|
|
24
|
+
}
|
|
25
|
+
if (typeof options.chunk === 'string' && options.chunk === 'BypassResult') {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
this.writeHead(options.statusCode, options.headers)
|
|
29
|
+
this.end(options.chunk)
|
|
30
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import { Injectable } from '@furystack/inject'
|
|
3
|
+
import { CorsOptions } from './models/cors-options'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A collection of various HTTP API related utilities
|
|
7
|
+
*/
|
|
8
|
+
@Injectable({ lifetime: 'transient' })
|
|
9
|
+
export class Utils {
|
|
10
|
+
public async readPostBodyRaw(incomingMessage: IncomingMessage) {
|
|
11
|
+
let body = ''
|
|
12
|
+
await new Promise<void>((resolve, reject) => {
|
|
13
|
+
incomingMessage.on('readable', () => {
|
|
14
|
+
const data = incomingMessage.read()
|
|
15
|
+
if (data) {
|
|
16
|
+
body += data
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
incomingMessage.on('end', () => {
|
|
20
|
+
resolve()
|
|
21
|
+
})
|
|
22
|
+
incomingMessage.on('error', (err) => {
|
|
23
|
+
reject(err)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
return body
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads the post's body and returns a promise with a parsed value
|
|
31
|
+
*
|
|
32
|
+
* @param incomingMessage The incoming message instance
|
|
33
|
+
* @returns the parsed object from the post body
|
|
34
|
+
*/
|
|
35
|
+
public async readPostBody<T>(incomingMessage: IncomingMessage): Promise<T> {
|
|
36
|
+
const body = await this.readPostBodyRaw(incomingMessage)
|
|
37
|
+
return JSON.parse(body) as T
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Adds the specified CORS headers to the response
|
|
42
|
+
*
|
|
43
|
+
* @param options The CORS Options object
|
|
44
|
+
* @param incomingMessage The incoming message instance
|
|
45
|
+
* @param serverResponse The outgoing response instance
|
|
46
|
+
*/
|
|
47
|
+
public addCorsHeaders(options: CorsOptions, incomingMessage: IncomingMessage, serverResponse: ServerResponse) {
|
|
48
|
+
if (
|
|
49
|
+
incomingMessage.headers &&
|
|
50
|
+
incomingMessage.headers.origin !== incomingMessage.headers.host &&
|
|
51
|
+
options.origins.some((origin) => origin === incomingMessage.headers.origin)
|
|
52
|
+
) {
|
|
53
|
+
serverResponse.setHeader('Access-Control-Allow-Origin', incomingMessage.headers.origin as string)
|
|
54
|
+
if (options.credentials) {
|
|
55
|
+
serverResponse.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
56
|
+
}
|
|
57
|
+
if (options.headers && options.headers.length) {
|
|
58
|
+
serverResponse.setHeader('Access-Control-Allow-Headers', options.headers.join(', '))
|
|
59
|
+
}
|
|
60
|
+
if (options.methods && options.methods.length) {
|
|
61
|
+
serverResponse.setHeader('Access-Control-Allow-Methods', options.methods.join(', '))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteEndpoint,
|
|
3
|
+
GetCollectionEndpoint,
|
|
4
|
+
GetEntityEndpoint,
|
|
5
|
+
PatchEndpoint,
|
|
6
|
+
PostEndpoint,
|
|
7
|
+
RestApi,
|
|
8
|
+
} from '@furystack/rest'
|
|
9
|
+
|
|
10
|
+
export interface Mock {
|
|
11
|
+
id: string
|
|
12
|
+
value: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ValidateQuery {
|
|
16
|
+
query: { foo: string; bar: number; baz: boolean }
|
|
17
|
+
result: { foo: string; bar: number; baz: boolean }
|
|
18
|
+
}
|
|
19
|
+
export interface ValidateUrl {
|
|
20
|
+
url: { id: number }
|
|
21
|
+
result: { id: number }
|
|
22
|
+
}
|
|
23
|
+
export interface ValidateHeaders {
|
|
24
|
+
headers: { foo: string; bar: number; baz: boolean }
|
|
25
|
+
result: { foo: string; bar: number; baz: boolean }
|
|
26
|
+
}
|
|
27
|
+
export interface ValidateBody {
|
|
28
|
+
body: { foo: string; bar: number; baz: boolean }
|
|
29
|
+
result: { foo: string; bar: number; baz: boolean }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ValidationApi extends RestApi {
|
|
33
|
+
GET: {
|
|
34
|
+
'/validate-query': ValidateQuery
|
|
35
|
+
'/validate-url/:id': ValidateUrl
|
|
36
|
+
'/validate-headers': ValidateHeaders
|
|
37
|
+
'/mock': GetCollectionEndpoint<Mock>
|
|
38
|
+
'/mock/:id': GetEntityEndpoint<Mock, 'id'>
|
|
39
|
+
}
|
|
40
|
+
POST: {
|
|
41
|
+
'/validate-body': ValidateBody
|
|
42
|
+
'/mock': PostEndpoint<Mock, 'id'>
|
|
43
|
+
}
|
|
44
|
+
PATCH: {
|
|
45
|
+
'/mock/:id': PatchEndpoint<Mock, 'id'>
|
|
46
|
+
}
|
|
47
|
+
DELETE: {
|
|
48
|
+
'/mock/:id': DeleteEndpoint<Mock, 'id'>
|
|
49
|
+
}
|
|
50
|
+
}
|