@rest-vir/define-service 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-CC0 +121 -0
- package/LICENSE-MIT +21 -0
- package/README.md +88 -0
- package/dist/augments/json.d.ts +8 -0
- package/dist/augments/json.js +13 -0
- package/dist/dev-port/find-dev-port.d.ts +132 -0
- package/dist/dev-port/find-dev-port.js +156 -0
- package/dist/endpoint/endpoint-path.d.ts +27 -0
- package/dist/endpoint/endpoint-path.js +14 -0
- package/dist/endpoint/endpoint.d.ts +198 -0
- package/dist/endpoint/endpoint.js +80 -0
- package/dist/frontend-connect/connect-web-socket.d.ts +95 -0
- package/dist/frontend-connect/connect-web-socket.js +64 -0
- package/dist/frontend-connect/fetch-endpoint.d.ts +199 -0
- package/dist/frontend-connect/fetch-endpoint.js +135 -0
- package/dist/frontend-connect/generate-api.d.ts +102 -0
- package/dist/frontend-connect/generate-api.js +83 -0
- package/dist/frontend-connect/mock-client-web-socket.d.ts +187 -0
- package/dist/frontend-connect/mock-client-web-socket.js +198 -0
- package/dist/frontend-connect/web-socket-protocol-parse.d.ts +10 -0
- package/dist/frontend-connect/web-socket-protocol-parse.js +111 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/service/define-service.d.ts +19 -0
- package/dist/service/define-service.js +133 -0
- package/dist/service/match-url.d.ts +30 -0
- package/dist/service/match-url.js +31 -0
- package/dist/service/minimal-service.d.ts +44 -0
- package/dist/service/minimal-service.js +1 -0
- package/dist/service/service-definition.d.ts +80 -0
- package/dist/service/service-definition.error.d.ts +35 -0
- package/dist/service/service-definition.error.js +34 -0
- package/dist/service/service-definition.js +1 -0
- package/dist/util/mock-fetch.d.ts +107 -0
- package/dist/util/mock-fetch.js +199 -0
- package/dist/util/no-param.d.ts +16 -0
- package/dist/util/no-param.js +8 -0
- package/dist/util/origin.d.ts +43 -0
- package/dist/util/origin.js +19 -0
- package/dist/util/path-to-regexp.d.ts +54 -0
- package/dist/util/path-to-regexp.js +307 -0
- package/dist/util/search-params.d.ts +9 -0
- package/dist/util/search-params.js +1 -0
- package/dist/web-socket/common-web-socket.d.ts +103 -0
- package/dist/web-socket/common-web-socket.js +28 -0
- package/dist/web-socket/overwrite-web-socket-methods.d.ts +276 -0
- package/dist/web-socket/overwrite-web-socket-methods.js +210 -0
- package/dist/web-socket/web-socket-definition.d.ts +170 -0
- package/dist/web-socket/web-socket-definition.js +78 -0
- package/package.json +68 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { getOrSet, } from '@augment-vir/common';
|
|
2
|
+
import { parseJsonWithUndefined } from '../augments/json.js';
|
|
3
|
+
import { CommonWebSocketState, } from '../web-socket/common-web-socket.js';
|
|
4
|
+
import { WebSocketLocation, } from '../web-socket/overwrite-web-socket-methods.js';
|
|
5
|
+
/**
|
|
6
|
+
* Use this to create a mock WebSocket constructor for unit testing Web Socket connection.
|
|
7
|
+
*
|
|
8
|
+
* This creates a {@link MockClientWebSocket} constructor for connections from the client side with
|
|
9
|
+
* types for the given {@link WebSocketDefinition} and utilizing the given
|
|
10
|
+
* {@link MockClientWebSocketOptions} instance. This can be passed to `connectWebSocket` as the
|
|
11
|
+
* `webSocketConstructor` to allow unit testing on a client without spinning up an entire host to
|
|
12
|
+
* serve the WebSocket connection.
|
|
13
|
+
*
|
|
14
|
+
* @category Testing : Client (Frontend)
|
|
15
|
+
* @category Package : @rest-vir/define-service
|
|
16
|
+
* @example
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import {
|
|
20
|
+
* connectWebSocket,
|
|
21
|
+
* createMockClientWebSocketConstructor,
|
|
22
|
+
* } from '@rest-vir/define-service';
|
|
23
|
+
*
|
|
24
|
+
* const webSocket = await connectWebSocket(myService.webSockets['/my-path'], {
|
|
25
|
+
* webSocketConstructor: createMockClientWebSocketConstructor(
|
|
26
|
+
* myService.webSockets['/my-path'],
|
|
27
|
+
* {preventImmediateOpen: true},
|
|
28
|
+
* ),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // mock server responses without an actual server
|
|
32
|
+
* webSocket.sendFromHost(myMessage);
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
36
|
+
*/
|
|
37
|
+
export function createMockClientWebSocketConstructor(webSocketToConnect, options = {}) {
|
|
38
|
+
return class extends MockClientWebSocket {
|
|
39
|
+
constructor(...params) {
|
|
40
|
+
super(...params, options);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A mock WebSocket constructor for connections from the client side. This can be passed to
|
|
46
|
+
* `connectWebSocket` as the `webSocketConstructor` to allow unit testing on a client without
|
|
47
|
+
* spinning up an entire host to serve the WebSocket connection.
|
|
48
|
+
*
|
|
49
|
+
* @category Internal
|
|
50
|
+
* @category Package : @rest-vir/define-service
|
|
51
|
+
* @example
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* import {connectWebSocket, MockClientWebSocket} from '@rest-vir/define-service';
|
|
55
|
+
*
|
|
56
|
+
* const webSocket = await connectWebSocket(myService.webSockets['/my-path'], {
|
|
57
|
+
* webSocketConstructor: MockClientWebSocket<(typeof myService.webSockets)['/my-path']>,
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // mock server responses without an actual server
|
|
61
|
+
* webSocket.sendFromHost(myMessage);
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
65
|
+
*/
|
|
66
|
+
export class MockClientWebSocket {
|
|
67
|
+
/**
|
|
68
|
+
* Mocked WebSocket event listeners. While this is public, you should attach a listener using
|
|
69
|
+
* {@link MockClientWebSocket.addEventListener} method and remove them with
|
|
70
|
+
* {@link MockClientWebSocket.removeEventListener}, as you would with a normal WebSocket
|
|
71
|
+
* instance.
|
|
72
|
+
*/
|
|
73
|
+
listeners = {};
|
|
74
|
+
/**
|
|
75
|
+
* Implements and mocks the standard `WebSocket.readyState` property.
|
|
76
|
+
*
|
|
77
|
+
* @see https://developer.mozilla.org/docs/Web/API/WebSocket/readyState
|
|
78
|
+
*/
|
|
79
|
+
readyState = CommonWebSocketState.Connecting;
|
|
80
|
+
/**
|
|
81
|
+
* This callback will be called whenever a message is sent to or from the WebSocket. This is set
|
|
82
|
+
* via the constructor option `sendCallback`.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* import {MockClientWebSocket, connectWebSocket} from '@rest-vir/define-service';
|
|
88
|
+
*
|
|
89
|
+
* const webSocket = await connectWebSocket(myService.webSockets['/my-socket'], {
|
|
90
|
+
* webSocketConstructor: createMockClientWebSocketConstructor(
|
|
91
|
+
* myService.webSockets['/my-socket'],
|
|
92
|
+
* {
|
|
93
|
+
* sendCallback: (message) => {
|
|
94
|
+
* // handle the sent message here
|
|
95
|
+
* console.log('handled message:', message);
|
|
96
|
+
* },
|
|
97
|
+
* },
|
|
98
|
+
* ),
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
sendCallback;
|
|
103
|
+
constructor(...[, , , options = {},]) {
|
|
104
|
+
if (!options.preventImmediateOpen) {
|
|
105
|
+
this.open();
|
|
106
|
+
}
|
|
107
|
+
if (options.sendCallback) {
|
|
108
|
+
this.sendCallback = options.sendCallback;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Manually set the WebSocket connection as opened. This is only necessary to use if you've set
|
|
113
|
+
* the constructor options to
|
|
114
|
+
*/
|
|
115
|
+
open() {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
if (this.readyState === CommonWebSocketState.Connecting) {
|
|
118
|
+
this.readyState = CommonWebSocketState.Open;
|
|
119
|
+
this.dispatchEvent('open', {});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Manually close and cleanup the WebSocket. */
|
|
124
|
+
close() {
|
|
125
|
+
this.dispatchEvent('close', {
|
|
126
|
+
code: 0,
|
|
127
|
+
reason: 'manually closed',
|
|
128
|
+
wasClean: true,
|
|
129
|
+
});
|
|
130
|
+
this.listeners = {};
|
|
131
|
+
this.readyState = CommonWebSocketState.Closed;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Dispatch a WebSocket event. This is primarily used within the MockClientWebSocket class
|
|
135
|
+
* itself but may also be used externally to test various WebSocket scenarios.
|
|
136
|
+
*
|
|
137
|
+
* Note that dispatching `'open'` and `'close'` events will not actually close or open the
|
|
138
|
+
* WebSocket. Use the {@link MockClientWebSocket.open} and {@link MockClientWebSocket.close}
|
|
139
|
+
* methods instead (which appropriately dispatch their events).
|
|
140
|
+
*/
|
|
141
|
+
dispatchEvent(eventName, event) {
|
|
142
|
+
this.listeners[eventName]?.forEach((listener) => listener({
|
|
143
|
+
...event,
|
|
144
|
+
target: this,
|
|
145
|
+
type: eventName,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Implements and mocks the standard `WebSocket.addEventListener` property but with message type
|
|
150
|
+
* safety.
|
|
151
|
+
*
|
|
152
|
+
* @see https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener
|
|
153
|
+
*/
|
|
154
|
+
addEventListener(eventName, listener) {
|
|
155
|
+
getOrSet(this.listeners, eventName, () => new Set()).add(listener);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Implements and mocks the standard `WebSocket.removeEventListener` property but with message
|
|
159
|
+
* type safety.
|
|
160
|
+
*
|
|
161
|
+
* @see https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener
|
|
162
|
+
*/
|
|
163
|
+
removeEventListener(eventName, listener) {
|
|
164
|
+
this.listeners[eventName]?.delete(listener);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Implements and mocks the standard `WebSocket.send` property but with message type safety.
|
|
168
|
+
*
|
|
169
|
+
* @see https://developer.mozilla.org/docs/Web/API/WebSocket/send
|
|
170
|
+
*/
|
|
171
|
+
send(data) {
|
|
172
|
+
if (this.readyState !== CommonWebSocketState.Open) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
void this.sendCallback?.({
|
|
176
|
+
messageData: parseJsonWithUndefined(data),
|
|
177
|
+
messageSource: WebSocketLocation.OnClient,
|
|
178
|
+
webSocket: this,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Sends a message as if it came from the WebSocket host. Use this to unit test client-side
|
|
183
|
+
* WebSockets without needing a running host.
|
|
184
|
+
*/
|
|
185
|
+
sendFromHost(data) {
|
|
186
|
+
if (this.readyState !== CommonWebSocketState.Open) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.dispatchEvent('message', {
|
|
190
|
+
data,
|
|
191
|
+
});
|
|
192
|
+
void this.sendCallback?.({
|
|
193
|
+
messageData: data,
|
|
194
|
+
messageSource: WebSocketLocation.OnHost,
|
|
195
|
+
webSocket: this,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. This is extracted
|
|
3
|
+
* from the [`ws`](https://www.npmjs.com/package/ws) package. This is used internally to verify if a
|
|
4
|
+
* set of protocols is valid.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
* @category Package : @rest-vir/define-service
|
|
8
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseSecWebSocketProtocolHeader(header: string): Set<string>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* eslint-disable unicorn/number-literal-case */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
3
|
+
// cspell:ignore Einar Stangvik Arnout Kazemier Pinca
|
|
4
|
+
/**
|
|
5
|
+
* This file's contents were extracted from the [`ws`](https://www.npmjs.com/package/ws) package's
|
|
6
|
+
* following files:
|
|
7
|
+
*
|
|
8
|
+
* - https://github.com/websockets/ws/blob/b9ca55b0aa8c72b39a778542bd0fa9b6c455d4c4/lib/subprotocol.js
|
|
9
|
+
* - https://github.com/websockets/ws/blob/b9ca55b0aa8c72b39a778542bd0fa9b6c455d4c4/lib/validation.js
|
|
10
|
+
*
|
|
11
|
+
* That package has the following license:
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
|
14
|
+
* Copyright (c) 2013 Arnout Kazemier and contributors
|
|
15
|
+
* Copyright (c) 2016 Luigi Pinca and contributors
|
|
16
|
+
*
|
|
17
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
18
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
19
|
+
* the Software without restriction, including without limitation the rights to
|
|
20
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
21
|
+
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
22
|
+
* subject to the following conditions:
|
|
23
|
+
*
|
|
24
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
25
|
+
* copies or substantial portions of the Software.
|
|
26
|
+
*
|
|
27
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
29
|
+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
30
|
+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
31
|
+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
32
|
+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
33
|
+
*/
|
|
34
|
+
//
|
|
35
|
+
// Allowed token characters:
|
|
36
|
+
//
|
|
37
|
+
// '!', '#', '$', '%', '&', ''', '*', '+', '-',
|
|
38
|
+
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
|
|
39
|
+
//
|
|
40
|
+
// tokenChars[32] === 0 // ' '
|
|
41
|
+
// tokenChars[33] === 1 // '!'
|
|
42
|
+
// tokenChars[34] === 0 // '"'
|
|
43
|
+
// ...
|
|
44
|
+
//
|
|
45
|
+
// prettier-ignore
|
|
46
|
+
const tokenChars = [
|
|
47
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
|
|
48
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
|
|
49
|
+
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
|
|
50
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
|
|
51
|
+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
|
|
52
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
|
|
53
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
|
|
54
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. This is extracted
|
|
58
|
+
* from the [`ws`](https://www.npmjs.com/package/ws) package. This is used internally to verify if a
|
|
59
|
+
* set of protocols is valid.
|
|
60
|
+
*
|
|
61
|
+
* @category Internal
|
|
62
|
+
* @category Package : @rest-vir/define-service
|
|
63
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
64
|
+
*/
|
|
65
|
+
export function parseSecWebSocketProtocolHeader(header) {
|
|
66
|
+
const protocols = new Set();
|
|
67
|
+
let start = -1;
|
|
68
|
+
let end = -1;
|
|
69
|
+
let i = 0;
|
|
70
|
+
for (i; i < header.length; i++) {
|
|
71
|
+
const code = header.codePointAt(i);
|
|
72
|
+
if (end === -1 && tokenChars[code] === 1) {
|
|
73
|
+
if (start === -1) {
|
|
74
|
+
start = i;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (i !== 0 && (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */) {
|
|
78
|
+
/* node:coverage ignore next 3: idk how to trigger this */
|
|
79
|
+
if (end === -1 && start !== -1) {
|
|
80
|
+
end = i;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (code === 0x2c /* ',' */) {
|
|
84
|
+
if (start === -1) {
|
|
85
|
+
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
86
|
+
}
|
|
87
|
+
if (end === -1) {
|
|
88
|
+
end = i;
|
|
89
|
+
}
|
|
90
|
+
const protocol = header.slice(start, end);
|
|
91
|
+
if (protocols.has(protocol)) {
|
|
92
|
+
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
|
|
93
|
+
}
|
|
94
|
+
protocols.add(protocol);
|
|
95
|
+
start = end = -1;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw new SyntaxError(`Unexpected character at index ${i}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/* node:coverage ignore next 3: idk how to trigger this */
|
|
102
|
+
if (start === -1 || end !== -1) {
|
|
103
|
+
throw new SyntaxError('Unexpected end of input');
|
|
104
|
+
}
|
|
105
|
+
const protocol = header.slice(start, i);
|
|
106
|
+
if (protocols.has(protocol)) {
|
|
107
|
+
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
|
|
108
|
+
}
|
|
109
|
+
protocols.add(protocol);
|
|
110
|
+
return protocols;
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { HttpMethod, HttpStatus } from '@augment-vir/common';
|
|
2
|
+
export * from './augments/json.js';
|
|
3
|
+
export * from './dev-port/find-dev-port.js';
|
|
4
|
+
export * from './endpoint/endpoint-path.js';
|
|
5
|
+
export * from './endpoint/endpoint.js';
|
|
6
|
+
export * from './frontend-connect/connect-web-socket.js';
|
|
7
|
+
export * from './frontend-connect/fetch-endpoint.js';
|
|
8
|
+
export * from './frontend-connect/generate-api.js';
|
|
9
|
+
export * from './frontend-connect/mock-client-web-socket.js';
|
|
10
|
+
export * from './frontend-connect/web-socket-protocol-parse.js';
|
|
11
|
+
export * from './service/define-service.js';
|
|
12
|
+
export * from './service/match-url.js';
|
|
13
|
+
export * from './service/minimal-service.js';
|
|
14
|
+
export * from './service/service-definition.error.js';
|
|
15
|
+
export * from './service/service-definition.js';
|
|
16
|
+
export * from './util/mock-fetch.js';
|
|
17
|
+
export * from './util/no-param.js';
|
|
18
|
+
export * from './util/origin.js';
|
|
19
|
+
export * from './util/path-to-regexp.js';
|
|
20
|
+
export * from './util/search-params.js';
|
|
21
|
+
export * from './web-socket/common-web-socket.js';
|
|
22
|
+
export * from './web-socket/overwrite-web-socket-methods.js';
|
|
23
|
+
export * from './web-socket/web-socket-definition.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { HttpMethod, HttpStatus } from '@augment-vir/common';
|
|
2
|
+
export * from './augments/json.js';
|
|
3
|
+
export * from './dev-port/find-dev-port.js';
|
|
4
|
+
export * from './endpoint/endpoint-path.js';
|
|
5
|
+
export * from './endpoint/endpoint.js';
|
|
6
|
+
export * from './frontend-connect/connect-web-socket.js';
|
|
7
|
+
export * from './frontend-connect/fetch-endpoint.js';
|
|
8
|
+
export * from './frontend-connect/generate-api.js';
|
|
9
|
+
export * from './frontend-connect/mock-client-web-socket.js';
|
|
10
|
+
export * from './frontend-connect/web-socket-protocol-parse.js';
|
|
11
|
+
export * from './service/define-service.js';
|
|
12
|
+
export * from './service/match-url.js';
|
|
13
|
+
export * from './service/minimal-service.js';
|
|
14
|
+
export * from './service/service-definition.error.js';
|
|
15
|
+
export * from './service/service-definition.js';
|
|
16
|
+
export * from './util/mock-fetch.js';
|
|
17
|
+
export * from './util/no-param.js';
|
|
18
|
+
export * from './util/origin.js';
|
|
19
|
+
export * from './util/path-to-regexp.js';
|
|
20
|
+
export * from './util/search-params.js';
|
|
21
|
+
export * from './web-socket/common-web-socket.js';
|
|
22
|
+
export * from './web-socket/overwrite-web-socket-methods.js';
|
|
23
|
+
export * from './web-socket/web-socket-definition.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BaseServiceEndpointsInit, BaseServiceWebSocketsInit, ServiceDefinition, ServiceInit } from './service-definition.js';
|
|
2
|
+
/**
|
|
3
|
+
* The main entry point to the whole `@rest-vir/define-service` package. This function accepts a
|
|
4
|
+
* {@link ServiceInit} object and returns a fully defined {@link ServiceDefinition}.
|
|
5
|
+
*
|
|
6
|
+
* @category Define Service
|
|
7
|
+
* @category Package : @rest-vir/define-service
|
|
8
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
9
|
+
*/
|
|
10
|
+
export declare function defineService<const ServiceName extends string, EndpointsInit extends BaseServiceEndpointsInit, WebSocketsInit extends BaseServiceWebSocketsInit>(serviceInit: ServiceInit<ServiceName, EndpointsInit, WebSocketsInit>): ServiceDefinition<ServiceName, EndpointsInit, WebSocketsInit>;
|
|
11
|
+
/**
|
|
12
|
+
* Asserts that the given input is a valid {@link ServiceDefinition} instance.
|
|
13
|
+
*
|
|
14
|
+
* @category Internal
|
|
15
|
+
* @category Package : @rest-vir/define-service
|
|
16
|
+
* @throws {@link ServiceDefinitionError} : if there is an issue
|
|
17
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertValidServiceDefinition(serviceDefinition: ServiceDefinition): asserts serviceDefinition is ServiceDefinition;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { assert, check } from '@augment-vir/assert';
|
|
2
|
+
import { getObjectTypedEntries, mapObjectValues, stringify } from '@augment-vir/common';
|
|
3
|
+
import { assertValidShape, defineShape } from 'object-shape-tester';
|
|
4
|
+
import { assertValidEndpoint, attachEndpointShapeTypeGetters, endpointInitShape, } from '../endpoint/endpoint.js';
|
|
5
|
+
import { assertValidWebSocketDefinition, attachWebSocketShapeTypeGetters, webSocketInitShape, } from '../web-socket/web-socket-definition.js';
|
|
6
|
+
import { ensureServiceDefinitionError } from './service-definition.error.js';
|
|
7
|
+
/**
|
|
8
|
+
* The main entry point to the whole `@rest-vir/define-service` package. This function accepts a
|
|
9
|
+
* {@link ServiceInit} object and returns a fully defined {@link ServiceDefinition}.
|
|
10
|
+
*
|
|
11
|
+
* @category Define Service
|
|
12
|
+
* @category Package : @rest-vir/define-service
|
|
13
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
14
|
+
*/
|
|
15
|
+
export function defineService(serviceInit) {
|
|
16
|
+
const serviceDefinition = finalizeServiceDefinition(serviceInit);
|
|
17
|
+
assertValidServiceDefinition(serviceDefinition);
|
|
18
|
+
return serviceDefinition;
|
|
19
|
+
}
|
|
20
|
+
function finalizeServiceDefinition(serviceInit) {
|
|
21
|
+
try {
|
|
22
|
+
const minimalService = {
|
|
23
|
+
serviceName: serviceInit.serviceName,
|
|
24
|
+
serviceOrigin: serviceInit.serviceOrigin,
|
|
25
|
+
requiredClientOrigin: serviceInit.requiredClientOrigin,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Make the types less strict because we don't care what they are inside of this function's
|
|
29
|
+
* implementation. Just the return type is what matters.
|
|
30
|
+
*/
|
|
31
|
+
const genericEndpoints = (serviceInit.endpoints || {});
|
|
32
|
+
const endpoints = mapObjectValues(genericEndpoints, (endpointPath, endpointInit) => {
|
|
33
|
+
assertValidShape({ searchParamsShape: undefined, ...endpointInit }, endpointInitShape);
|
|
34
|
+
const endpoint = {
|
|
35
|
+
...endpointInit,
|
|
36
|
+
requestDataShape: endpointInit.requestDataShape
|
|
37
|
+
? defineShape(endpointInit.requestDataShape, true)
|
|
38
|
+
: undefined,
|
|
39
|
+
responseDataShape: endpointInit.responseDataShape
|
|
40
|
+
? defineShape(endpointInit.responseDataShape, true)
|
|
41
|
+
: undefined,
|
|
42
|
+
path: endpointPath,
|
|
43
|
+
service: minimalService,
|
|
44
|
+
customProps: endpointInit.customProps,
|
|
45
|
+
isEndpoint: true,
|
|
46
|
+
isWebSocket: false,
|
|
47
|
+
searchParamsShape: (endpointInit.searchParamsShape
|
|
48
|
+
? defineShape(endpointInit.searchParamsShape)
|
|
49
|
+
: undefined),
|
|
50
|
+
};
|
|
51
|
+
attachEndpointShapeTypeGetters(endpoint);
|
|
52
|
+
return endpoint;
|
|
53
|
+
});
|
|
54
|
+
const genericWebSockets = (serviceInit.webSockets || {});
|
|
55
|
+
const webSockets = mapObjectValues(genericWebSockets, (webSocketPath, webSocketInit) => {
|
|
56
|
+
assertValidShape({ protocolsShape: undefined, searchParamsShape: undefined, ...webSocketInit }, webSocketInitShape);
|
|
57
|
+
const webSocketDefinition = {
|
|
58
|
+
...webSocketInit,
|
|
59
|
+
messageFromClientShape: webSocketInit.messageFromClientShape
|
|
60
|
+
? defineShape(webSocketInit.messageFromClientShape)
|
|
61
|
+
: undefined,
|
|
62
|
+
messageFromHostShape: webSocketInit.messageFromHostShape
|
|
63
|
+
? defineShape(webSocketInit.messageFromHostShape)
|
|
64
|
+
: undefined,
|
|
65
|
+
protocolsShape: (webSocketInit.protocolsShape
|
|
66
|
+
? defineShape(webSocketInit.protocolsShape)
|
|
67
|
+
: undefined),
|
|
68
|
+
searchParamsShape: (webSocketInit.searchParamsShape
|
|
69
|
+
? defineShape(webSocketInit.searchParamsShape)
|
|
70
|
+
: undefined),
|
|
71
|
+
service: minimalService,
|
|
72
|
+
path: webSocketPath,
|
|
73
|
+
customProps: webSocketInit.customProps,
|
|
74
|
+
isEndpoint: false,
|
|
75
|
+
isWebSocket: true,
|
|
76
|
+
};
|
|
77
|
+
attachWebSocketShapeTypeGetters(webSocketDefinition);
|
|
78
|
+
return webSocketDefinition;
|
|
79
|
+
});
|
|
80
|
+
const serviceDefinition = {
|
|
81
|
+
serviceName: serviceInit.serviceName,
|
|
82
|
+
serviceOrigin: serviceInit.serviceOrigin,
|
|
83
|
+
init: {
|
|
84
|
+
...serviceInit,
|
|
85
|
+
webSockets: (serviceInit.webSockets || {}),
|
|
86
|
+
endpoints: (serviceInit.endpoints || {}),
|
|
87
|
+
},
|
|
88
|
+
webSockets: webSockets,
|
|
89
|
+
requiredClientOrigin: serviceInit.requiredClientOrigin,
|
|
90
|
+
/** As cast needed again to narrow the type (for the return value) after broadening it. */
|
|
91
|
+
endpoints: endpoints,
|
|
92
|
+
};
|
|
93
|
+
return serviceDefinition;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw ensureServiceDefinitionError(error, {
|
|
97
|
+
path: undefined,
|
|
98
|
+
serviceName: serviceInit.serviceName,
|
|
99
|
+
isEndpoint: undefined,
|
|
100
|
+
isWebSocket: undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Asserts that the given input is a valid {@link ServiceDefinition} instance.
|
|
106
|
+
*
|
|
107
|
+
* @category Internal
|
|
108
|
+
* @category Package : @rest-vir/define-service
|
|
109
|
+
* @throws {@link ServiceDefinitionError} : if there is an issue
|
|
110
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
111
|
+
*/
|
|
112
|
+
export function assertValidServiceDefinition(serviceDefinition) {
|
|
113
|
+
try {
|
|
114
|
+
if (!serviceDefinition.serviceName || !check.isString(serviceDefinition.serviceName)) {
|
|
115
|
+
throw new Error(`Invalid service name: '${stringify(serviceDefinition.serviceName)}'. Expected a non-empty string.`);
|
|
116
|
+
}
|
|
117
|
+
assert.isDefined(serviceDefinition.requiredClientOrigin);
|
|
118
|
+
getObjectTypedEntries(serviceDefinition.endpoints).forEach(([, endpoint,]) => {
|
|
119
|
+
assertValidEndpoint(endpoint);
|
|
120
|
+
});
|
|
121
|
+
getObjectTypedEntries(serviceDefinition.webSockets).forEach(([, webSocketDefinition,]) => {
|
|
122
|
+
assertValidWebSocketDefinition(webSocketDefinition);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw ensureServiceDefinitionError(error, {
|
|
127
|
+
path: undefined,
|
|
128
|
+
serviceName: serviceDefinition.serviceName,
|
|
129
|
+
isEndpoint: undefined,
|
|
130
|
+
isWebSocket: undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type RequireAtLeastOne } from 'type-fest';
|
|
2
|
+
import { EndpointPathBase } from '../endpoint/endpoint-path.js';
|
|
3
|
+
/**
|
|
4
|
+
* Given a raw path or URL, finds an endpoint or WebSocket path that will match in the given
|
|
5
|
+
* service. If no match is found, this returns `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
* @category Package : @rest-vir/implement-service
|
|
9
|
+
* @package [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service)
|
|
10
|
+
*/
|
|
11
|
+
export declare function matchUrlToService<const Service extends Readonly<{
|
|
12
|
+
webSockets: Record<EndpointPathBase, any>;
|
|
13
|
+
endpoints: Record<EndpointPathBase, any>;
|
|
14
|
+
}>>(this: void, service: Service,
|
|
15
|
+
/** The URL or path to match against. */
|
|
16
|
+
url: string): MatchedServicePath<Service> | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Output for {@link matchUrlToService}.
|
|
19
|
+
*
|
|
20
|
+
* @category Internal
|
|
21
|
+
* @category Package : @rest-vir/implement-service
|
|
22
|
+
* @package [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service)
|
|
23
|
+
*/
|
|
24
|
+
export type MatchedServicePath<Service extends Readonly<{
|
|
25
|
+
webSockets: Record<EndpointPathBase, any>;
|
|
26
|
+
endpoints: Record<EndpointPathBase, any>;
|
|
27
|
+
}>> = RequireAtLeastOne<{
|
|
28
|
+
webSocketPath: keyof Service['webSockets'];
|
|
29
|
+
endpointPath: keyof Service['endpoints'];
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getObjectTypedKeys } from '@augment-vir/common';
|
|
2
|
+
import { parseUrl } from 'url-vir';
|
|
3
|
+
import { match } from '../util/path-to-regexp.js';
|
|
4
|
+
/**
|
|
5
|
+
* Given a raw path or URL, finds an endpoint or WebSocket path that will match in the given
|
|
6
|
+
* service. If no match is found, this returns `undefined`.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
* @category Package : @rest-vir/implement-service
|
|
10
|
+
* @package [`@rest-vir/implement-service`](https://www.npmjs.com/package/@rest-vir/implement-service)
|
|
11
|
+
*/
|
|
12
|
+
export function matchUrlToService(service,
|
|
13
|
+
/** The URL or path to match against. */
|
|
14
|
+
url) {
|
|
15
|
+
const { pathname } = parseUrl(url);
|
|
16
|
+
const endpointPath = getObjectTypedKeys(service.endpoints).find((endpointPath) => {
|
|
17
|
+
return match(endpointPath)(pathname);
|
|
18
|
+
});
|
|
19
|
+
const webSocketPath = getObjectTypedKeys(service.webSockets).find((webSocketPath) => {
|
|
20
|
+
return match(webSocketPath)(pathname);
|
|
21
|
+
});
|
|
22
|
+
if (!endpointPath && !webSocketPath) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return {
|
|
27
|
+
...(endpointPath ? { endpointPath } : {}),
|
|
28
|
+
...(webSocketPath ? { webSocketPath: webSocketPath } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type IsEqual } from 'type-fest';
|
|
2
|
+
import { type OriginRequirement } from '../util/origin.js';
|
|
3
|
+
/**
|
|
4
|
+
* This is a minimal service definition with only data to send and handle fetch requests to that
|
|
5
|
+
* service. Each endpoint definition gets a copy of this embedded into it so the endpoint alone can
|
|
6
|
+
* be used as an input to the endpoint fetch function.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
* @category Package : @rest-vir/define-service
|
|
10
|
+
* @package [`@rest-vir/define-service`](https://www.npmjs.com/package/@rest-vir/define-service)
|
|
11
|
+
*/
|
|
12
|
+
export type MinimalService<ServiceName extends string = string> = {
|
|
13
|
+
serviceName: IsEqual<ServiceName, ''> extends true ? never : ServiceName;
|
|
14
|
+
/**
|
|
15
|
+
* The origin at which the service will be hosted. Fetch requests and WebSocket connections will
|
|
16
|
+
* be sent to this service will be sent to this origin.
|
|
17
|
+
*
|
|
18
|
+
* It is recommended to use a ternary to switch between dev and prod origins.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import {defineService} from '@rest-vir/define-service';
|
|
24
|
+
*
|
|
25
|
+
* defineService({
|
|
26
|
+
* serviceOrigin: isDev ? 'http://localhost:3000' : 'https://example.com',
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Location for help on which part of the URL is the origin (if necessary).
|
|
31
|
+
*/
|
|
32
|
+
serviceOrigin: string;
|
|
33
|
+
/**
|
|
34
|
+
* The service's `origin` requirement for all endpoint requests and WebSocket connections. This
|
|
35
|
+
* is used for CORS handshakes.
|
|
36
|
+
*
|
|
37
|
+
* This can be a string, a RegExp, a function, or an array of any of those. (If this is an
|
|
38
|
+
* array, the first matching array element will be used.)
|
|
39
|
+
*
|
|
40
|
+
* Set this to `AnyOrigin` (imported from `'@rest-vir/define-service'`) to allow any origins.
|
|
41
|
+
* Make sure that you're okay with the security impact this may have on your users of doing so.
|
|
42
|
+
*/
|
|
43
|
+
requiredClientOrigin: NonNullable<OriginRequirement>;
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|