eventsourcingdb 1.8.1 → 1.8.3
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/dist/Bound.d.ts +6 -0
- package/dist/Bound.js +1 -0
- package/dist/Client.d.ts +21 -0
- package/dist/Client.js +484 -0
- package/dist/CloudEvent.d.ts +16 -0
- package/dist/CloudEvent.js +1 -0
- package/dist/Container.d.ts +20 -0
- package/dist/Container.js +108 -0
- package/dist/Event.d.ts +36 -0
- package/dist/Event.js +63 -0
- package/dist/EventCandidate.d.ts +9 -0
- package/dist/EventCandidate.js +1 -0
- package/dist/EventType.d.ts +6 -0
- package/dist/EventType.js +1 -0
- package/dist/ObserveEventsOptions.d.ts +13 -0
- package/dist/ObserveEventsOptions.js +1 -0
- package/dist/Precondition.d.ts +27 -0
- package/dist/Precondition.js +1 -0
- package/dist/ReadEventsOptions.d.ts +16 -0
- package/dist/ReadEventsOptions.js +1 -0
- package/dist/convertCloudEventToEvent.d.ts +4 -0
- package/dist/convertCloudEventToEvent.js +18 -0
- package/dist/getImageVersionFromDockerfile.d.ts +2 -0
- package/dist/getImageVersionFromDockerfile.js +14 -0
- package/dist/index.d.ts +14 -154
- package/dist/index.js +7 -2
- package/dist/isCloudEvent.d.ts +3 -0
- package/dist/isCloudEvent.js +21 -0
- package/dist/isEventQlQueryTrue.d.ts +3 -0
- package/dist/isEventQlQueryTrue.js +5 -0
- package/dist/isSubjectOnEventId.d.ts +3 -0
- package/dist/isSubjectOnEventId.js +5 -0
- package/dist/isSubjectPopulated.d.ts +3 -0
- package/dist/isSubjectPopulated.js +5 -0
- package/dist/isSubjectPristine.d.ts +3 -0
- package/dist/isSubjectPristine.js +5 -0
- package/dist/isValidServerHeader.d.ts +2 -0
- package/dist/isValidServerHeader.js +11 -0
- package/dist/ndjson/readNdJsonStream.d.ts +2 -0
- package/dist/ndjson/readNdJsonStream.js +43 -0
- package/dist/stream/StreamCloudEvent.d.ts +19 -0
- package/dist/stream/StreamCloudEvent.js +1 -0
- package/dist/stream/StreamError.d.ts +7 -0
- package/dist/stream/StreamError.js +1 -0
- package/dist/stream/StreamEventType.d.ts +9 -0
- package/dist/stream/StreamEventType.js +1 -0
- package/dist/stream/StreamHeartbeat.d.ts +4 -0
- package/dist/stream/StreamHeartbeat.js +1 -0
- package/dist/stream/StreamRow.d.ts +5 -0
- package/dist/stream/StreamRow.js +1 -0
- package/dist/stream/StreamSubject.d.ts +7 -0
- package/dist/stream/StreamSubject.js +1 -0
- package/dist/stream/isStreamCloudEvent.d.ts +3 -0
- package/dist/stream/isStreamCloudEvent.js +24 -0
- package/dist/stream/isStreamError.d.ts +3 -0
- package/dist/stream/isStreamError.js +9 -0
- package/dist/stream/isStreamEventType.d.ts +3 -0
- package/dist/stream/isStreamEventType.js +11 -0
- package/dist/stream/isStreamHeartbeat.d.ts +3 -0
- package/dist/stream/isStreamHeartbeat.js +5 -0
- package/dist/stream/isStreamRow.d.ts +3 -0
- package/dist/stream/isStreamRow.js +6 -0
- package/dist/stream/isStreamSubject.d.ts +3 -0
- package/dist/stream/isStreamSubject.js +9 -0
- package/dist/types/hasShapeOf.d.ts +6 -0
- package/dist/types/hasShapeOf.js +25 -0
- package/dist/types/isBoolean.d.ts +2 -0
- package/dist/types/isBoolean.js +7 -0
- package/dist/types/isRecord.d.ts +2 -0
- package/dist/types/isRecord.js +13 -0
- package/dist/types/isString.d.ts +2 -0
- package/dist/types/isString.js +7 -0
- package/dist/types/isStringOrNull.d.ts +2 -0
- package/dist/types/isStringOrNull.js +10 -0
- package/dist/types/isStringOrUndefined.d.ts +2 -0
- package/dist/types/isStringOrUndefined.js +10 -0
- package/package.json +14 -11
- package/.editorconfig +0 -22
- package/dist/index.d.mts +0 -154
- package/dist/index.mjs +0 -2
package/dist/Bound.d.ts
ADDED
package/dist/Bound.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/Client.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Event } from './Event.js';
|
|
2
|
+
import type { EventCandidate } from './EventCandidate.js';
|
|
3
|
+
import type { EventType } from './EventType.js';
|
|
4
|
+
import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
|
|
5
|
+
import type { Precondition } from './Precondition.js';
|
|
6
|
+
import type { ReadEventsOptions } from './ReadEventsOptions.js';
|
|
7
|
+
declare class Client {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(url: URL, apiToken: string);
|
|
10
|
+
ping(): Promise<void>;
|
|
11
|
+
verifyApiToken(): Promise<void>;
|
|
12
|
+
writeEvents(events: EventCandidate[], preconditions?: Precondition[]): Promise<Event[]>;
|
|
13
|
+
readEvents(subject: string, options: ReadEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
|
|
14
|
+
runEventQlQuery(query: string, signal?: AbortSignal): AsyncGenerator<unknown, void, void>;
|
|
15
|
+
observeEvents(subject: string, options: ObserveEventsOptions, signal?: AbortSignal): AsyncGenerator<Event, void, void>;
|
|
16
|
+
registerEventSchema(eventType: string, schema: Record<string, unknown>): Promise<void>;
|
|
17
|
+
readSubjects(baseSubject: string, signal?: AbortSignal): AsyncGenerator<string, void, void>;
|
|
18
|
+
readEventType(eventType: string): Promise<EventType>;
|
|
19
|
+
readEventTypes(signal?: AbortSignal): AsyncGenerator<EventType, void, void>;
|
|
20
|
+
}
|
|
21
|
+
export { Client };
|
package/dist/Client.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
|
|
2
|
+
import { isCloudEvent } from './isCloudEvent.js';
|
|
3
|
+
import { isValidServerHeader } from './isValidServerHeader.js';
|
|
4
|
+
import { readNdJsonStream } from './ndjson/readNdJsonStream.js';
|
|
5
|
+
import { isStreamCloudEvent } from './stream/isStreamCloudEvent.js';
|
|
6
|
+
import { isStreamError } from './stream/isStreamError.js';
|
|
7
|
+
import { isStreamEventType } from './stream/isStreamEventType.js';
|
|
8
|
+
import { isStreamHeartbeat } from './stream/isStreamHeartbeat.js';
|
|
9
|
+
import { isStreamRow } from './stream/isStreamRow.js';
|
|
10
|
+
import { isStreamSubject } from './stream/isStreamSubject.js';
|
|
11
|
+
import { hasShapeOf } from './types/hasShapeOf.js';
|
|
12
|
+
import { isBoolean } from './types/isBoolean.js';
|
|
13
|
+
import { isString } from './types/isString.js';
|
|
14
|
+
class Client {
|
|
15
|
+
#url;
|
|
16
|
+
#apiToken;
|
|
17
|
+
#getUrl(path) {
|
|
18
|
+
return new URL(path, this.#url).toString();
|
|
19
|
+
}
|
|
20
|
+
constructor(url, apiToken) {
|
|
21
|
+
this.#url = url;
|
|
22
|
+
this.#apiToken = apiToken;
|
|
23
|
+
}
|
|
24
|
+
async ping() {
|
|
25
|
+
const url = this.#getUrl('/api/v1/ping');
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: 'get',
|
|
28
|
+
});
|
|
29
|
+
if (!isValidServerHeader(response)) {
|
|
30
|
+
throw new Error('Server must be EventSourcingDB.');
|
|
31
|
+
}
|
|
32
|
+
if (response.status !== 200) {
|
|
33
|
+
throw new Error(`Failed to ping, got HTTP status code '${response.status}', expected '200'.`);
|
|
34
|
+
}
|
|
35
|
+
const responseBody = await response.json();
|
|
36
|
+
if (!hasShapeOf(responseBody, {
|
|
37
|
+
type: isString,
|
|
38
|
+
})) {
|
|
39
|
+
throw new Error('Failed to parse response.');
|
|
40
|
+
}
|
|
41
|
+
const eventType = 'io.eventsourcingdb.api.ping-received';
|
|
42
|
+
if (responseBody.type !== eventType) {
|
|
43
|
+
throw new Error('Failed to ping.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async verifyApiToken() {
|
|
47
|
+
const url = this.#getUrl('/api/v1/verify-api-token');
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: 'post',
|
|
50
|
+
headers: {
|
|
51
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (!isValidServerHeader(response)) {
|
|
55
|
+
throw new Error('Server must be EventSourcingDB.');
|
|
56
|
+
}
|
|
57
|
+
if (response.status !== 200) {
|
|
58
|
+
throw new Error(`Failed to verify API token, got HTTP status code '${response.status}', expected '200'.`);
|
|
59
|
+
}
|
|
60
|
+
const responseBody = await response.json();
|
|
61
|
+
if (!hasShapeOf(responseBody, {
|
|
62
|
+
type: isString,
|
|
63
|
+
})) {
|
|
64
|
+
throw new Error('Failed to parse response.');
|
|
65
|
+
}
|
|
66
|
+
const eventType = 'io.eventsourcingdb.api.api-token-verified';
|
|
67
|
+
if (responseBody.type !== eventType) {
|
|
68
|
+
throw new Error('Failed to verify API token.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async writeEvents(events, preconditions = []) {
|
|
72
|
+
const url = this.#getUrl('/api/v1/write-events');
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
method: 'post',
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
events,
|
|
81
|
+
preconditions,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
if (!isValidServerHeader(response)) {
|
|
85
|
+
throw new Error('Server must be EventSourcingDB.');
|
|
86
|
+
}
|
|
87
|
+
if (response.status !== 200) {
|
|
88
|
+
throw new Error(`Failed to write events, got HTTP status code '${response.status}', expected '200'.`);
|
|
89
|
+
}
|
|
90
|
+
const responseBody = await response.json();
|
|
91
|
+
if (!Array.isArray(responseBody)) {
|
|
92
|
+
throw new Error('Failed to parse response.');
|
|
93
|
+
}
|
|
94
|
+
const writtenEvents = responseBody.map(item => {
|
|
95
|
+
if (!isCloudEvent(item)) {
|
|
96
|
+
throw new Error('Failed to parse response item.');
|
|
97
|
+
}
|
|
98
|
+
const event = convertCloudEventToEvent(item);
|
|
99
|
+
return event;
|
|
100
|
+
});
|
|
101
|
+
return writtenEvents;
|
|
102
|
+
}
|
|
103
|
+
readEvents(subject, options, signal) {
|
|
104
|
+
const url = this.#getUrl('/api/v1/read-events');
|
|
105
|
+
const apiToken = this.#apiToken;
|
|
106
|
+
return (async function* () {
|
|
107
|
+
const internalAbortController = new AbortController();
|
|
108
|
+
const combinedSignal = signal ?? internalAbortController.signal;
|
|
109
|
+
const shouldAbortInternally = !signal;
|
|
110
|
+
let removeAbortListener;
|
|
111
|
+
if (signal && !signal.aborted) {
|
|
112
|
+
const onAbort = () => internalAbortController.abort();
|
|
113
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
114
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: 'post',
|
|
119
|
+
headers: {
|
|
120
|
+
authorization: `Bearer ${apiToken}`,
|
|
121
|
+
'content-type': 'application/json',
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
subject,
|
|
125
|
+
options,
|
|
126
|
+
}),
|
|
127
|
+
signal: combinedSignal,
|
|
128
|
+
});
|
|
129
|
+
if (!isValidServerHeader(response)) {
|
|
130
|
+
const serverHeader = response.headers.get('Server');
|
|
131
|
+
if (!serverHeader) {
|
|
132
|
+
throw new Error('Server must be EventSourcingDB, but Server header is missing.');
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
|
|
135
|
+
}
|
|
136
|
+
if (response.status !== 200) {
|
|
137
|
+
throw new Error(`Failed to read events, got HTTP status code '${response.status}', expected '200'.`);
|
|
138
|
+
}
|
|
139
|
+
if (!response.body) {
|
|
140
|
+
throw new Error('Failed to read events.');
|
|
141
|
+
}
|
|
142
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
143
|
+
if (isStreamError(line)) {
|
|
144
|
+
throw new Error(`${line.payload.error}.`);
|
|
145
|
+
}
|
|
146
|
+
if (isStreamCloudEvent(line)) {
|
|
147
|
+
const event = convertCloudEventToEvent(line.payload);
|
|
148
|
+
yield event;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw new Error('Failed to read events.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
if (removeAbortListener) {
|
|
162
|
+
removeAbortListener();
|
|
163
|
+
}
|
|
164
|
+
if (shouldAbortInternally) {
|
|
165
|
+
internalAbortController.abort();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
}
|
|
170
|
+
runEventQlQuery(query, signal) {
|
|
171
|
+
const url = this.#getUrl('/api/v1/run-eventql-query');
|
|
172
|
+
const apiToken = this.#apiToken;
|
|
173
|
+
return (async function* () {
|
|
174
|
+
const internalAbortController = new AbortController();
|
|
175
|
+
const combinedSignal = signal ?? internalAbortController.signal;
|
|
176
|
+
const shouldAbortInternally = !signal;
|
|
177
|
+
let removeAbortListener;
|
|
178
|
+
if (signal && !signal.aborted) {
|
|
179
|
+
const onAbort = () => internalAbortController.abort();
|
|
180
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
181
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(url, {
|
|
185
|
+
method: 'post',
|
|
186
|
+
headers: {
|
|
187
|
+
authorization: `Bearer ${apiToken}`,
|
|
188
|
+
'content-type': 'application/json',
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ query }),
|
|
191
|
+
signal: combinedSignal,
|
|
192
|
+
});
|
|
193
|
+
if (!isValidServerHeader(response)) {
|
|
194
|
+
const serverHeader = response.headers.get('Server');
|
|
195
|
+
if (!serverHeader) {
|
|
196
|
+
throw new Error('Server must be EventSourcingDB, but Server header is missing.');
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
|
|
199
|
+
}
|
|
200
|
+
if (response.status !== 200) {
|
|
201
|
+
throw new Error(`Failed to run EventQL query, got HTTP status code '${response.status}', expected '200'.`);
|
|
202
|
+
}
|
|
203
|
+
if (!response.body) {
|
|
204
|
+
throw new Error('Failed to run EventQL query.');
|
|
205
|
+
}
|
|
206
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
207
|
+
if (isStreamHeartbeat(line)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (isStreamError(line)) {
|
|
211
|
+
throw new Error(`${line.payload.error}.`);
|
|
212
|
+
}
|
|
213
|
+
if (isStreamRow(line)) {
|
|
214
|
+
const row = line.payload;
|
|
215
|
+
yield row;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
throw new Error('Failed to run EventQL query.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (removeAbortListener) {
|
|
229
|
+
removeAbortListener();
|
|
230
|
+
}
|
|
231
|
+
if (shouldAbortInternally) {
|
|
232
|
+
internalAbortController.abort();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
}
|
|
237
|
+
observeEvents(subject, options, signal) {
|
|
238
|
+
const url = this.#getUrl('/api/v1/observe-events');
|
|
239
|
+
const apiToken = this.#apiToken;
|
|
240
|
+
return (async function* () {
|
|
241
|
+
const internalAbortController = new AbortController();
|
|
242
|
+
const combinedSignal = signal ?? internalAbortController.signal;
|
|
243
|
+
const shouldAbortInternally = !signal;
|
|
244
|
+
let removeAbortListener;
|
|
245
|
+
if (signal && !signal.aborted) {
|
|
246
|
+
const onAbort = () => internalAbortController.abort();
|
|
247
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
248
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
method: 'post',
|
|
253
|
+
headers: {
|
|
254
|
+
authorization: `Bearer ${apiToken}`,
|
|
255
|
+
'content-type': 'application/json',
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
subject,
|
|
259
|
+
options,
|
|
260
|
+
}),
|
|
261
|
+
signal: combinedSignal,
|
|
262
|
+
});
|
|
263
|
+
if (!isValidServerHeader(response)) {
|
|
264
|
+
const serverHeader = response.headers.get('Server');
|
|
265
|
+
if (!serverHeader) {
|
|
266
|
+
throw new Error('Server must be EventSourcingDB, but Server header is missing.');
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
|
|
269
|
+
}
|
|
270
|
+
if (response.status !== 200) {
|
|
271
|
+
throw new Error(`Failed to observe events, got HTTP status code '${response.status}', expected '200'.`);
|
|
272
|
+
}
|
|
273
|
+
if (!response.body) {
|
|
274
|
+
throw new Error('Failed to observe events.');
|
|
275
|
+
}
|
|
276
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
277
|
+
if (isStreamHeartbeat(line)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (isStreamError(line)) {
|
|
281
|
+
throw new Error(`${line.payload.error}.`);
|
|
282
|
+
}
|
|
283
|
+
if (isStreamCloudEvent(line)) {
|
|
284
|
+
const event = convertCloudEventToEvent(line.payload);
|
|
285
|
+
yield event;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
throw new Error('Failed to observe events.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (removeAbortListener) {
|
|
299
|
+
removeAbortListener();
|
|
300
|
+
}
|
|
301
|
+
if (shouldAbortInternally) {
|
|
302
|
+
internalAbortController.abort();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
}
|
|
307
|
+
async registerEventSchema(eventType, schema) {
|
|
308
|
+
const url = this.#getUrl('/api/v1/register-event-schema');
|
|
309
|
+
const response = await fetch(url, {
|
|
310
|
+
method: 'post',
|
|
311
|
+
headers: {
|
|
312
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
313
|
+
'content-type': 'application/json',
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
eventType,
|
|
317
|
+
schema,
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
if (!isValidServerHeader(response)) {
|
|
321
|
+
throw new Error('Server must be EventSourcingDB.');
|
|
322
|
+
}
|
|
323
|
+
if (response.status !== 200) {
|
|
324
|
+
throw new Error(`Failed to register event schema, got HTTP status code '${response.status}', expected '200'.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
readSubjects(baseSubject, signal) {
|
|
328
|
+
const url = this.#getUrl('/api/v1/read-subjects');
|
|
329
|
+
const apiToken = this.#apiToken;
|
|
330
|
+
return (async function* () {
|
|
331
|
+
const internalAbortController = new AbortController();
|
|
332
|
+
const combinedSignal = signal ?? internalAbortController.signal;
|
|
333
|
+
const shouldAbortInternally = !signal;
|
|
334
|
+
let removeAbortListener;
|
|
335
|
+
if (signal && !signal.aborted) {
|
|
336
|
+
const onAbort = () => internalAbortController.abort();
|
|
337
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
338
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetch(url, {
|
|
342
|
+
method: 'post',
|
|
343
|
+
headers: {
|
|
344
|
+
authorization: `Bearer ${apiToken}`,
|
|
345
|
+
'content-type': 'application/json',
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
baseSubject,
|
|
349
|
+
}),
|
|
350
|
+
signal: combinedSignal,
|
|
351
|
+
});
|
|
352
|
+
if (!isValidServerHeader(response)) {
|
|
353
|
+
const serverHeader = response.headers.get('Server');
|
|
354
|
+
if (!serverHeader) {
|
|
355
|
+
throw new Error('Server must be EventSourcingDB, but Server header is missing.');
|
|
356
|
+
}
|
|
357
|
+
throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
|
|
358
|
+
}
|
|
359
|
+
if (response.status !== 200) {
|
|
360
|
+
throw new Error(`Failed to read subjects, got HTTP status code '${response.status}', expected '200'.`);
|
|
361
|
+
}
|
|
362
|
+
if (!response.body) {
|
|
363
|
+
throw new Error('Failed to read subjects.');
|
|
364
|
+
}
|
|
365
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
366
|
+
if (isStreamError(line)) {
|
|
367
|
+
throw new Error(`${line.payload.error}.`);
|
|
368
|
+
}
|
|
369
|
+
if (isStreamSubject(line)) {
|
|
370
|
+
yield line.payload.subject;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
throw new Error('Failed to read subjects.');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (removeAbortListener) {
|
|
384
|
+
removeAbortListener();
|
|
385
|
+
}
|
|
386
|
+
if (shouldAbortInternally) {
|
|
387
|
+
internalAbortController.abort();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
}
|
|
392
|
+
async readEventType(eventType) {
|
|
393
|
+
const url = this.#getUrl('/api/v1/read-event-type');
|
|
394
|
+
const response = await fetch(url, {
|
|
395
|
+
method: 'post',
|
|
396
|
+
headers: {
|
|
397
|
+
authorization: `Bearer ${this.#apiToken}`,
|
|
398
|
+
'content-type': 'application/json',
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
eventType,
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
if (!isValidServerHeader(response)) {
|
|
405
|
+
throw new Error('Server must be EventSourcingDB.');
|
|
406
|
+
}
|
|
407
|
+
if (response.status !== 200) {
|
|
408
|
+
throw new Error(`Failed to read event type, got HTTP status code '${response.status}', expected '200'.`);
|
|
409
|
+
}
|
|
410
|
+
const responseBody = await response.json();
|
|
411
|
+
if (!hasShapeOf(responseBody, {
|
|
412
|
+
eventType: isString,
|
|
413
|
+
isPhantom: isBoolean,
|
|
414
|
+
})) {
|
|
415
|
+
throw new Error('Failed to parse response.');
|
|
416
|
+
}
|
|
417
|
+
return responseBody;
|
|
418
|
+
}
|
|
419
|
+
readEventTypes(signal) {
|
|
420
|
+
const url = this.#getUrl('/api/v1/read-event-types');
|
|
421
|
+
const apiToken = this.#apiToken;
|
|
422
|
+
return (async function* () {
|
|
423
|
+
const internalAbortController = new AbortController();
|
|
424
|
+
const combinedSignal = signal ?? internalAbortController.signal;
|
|
425
|
+
const shouldAbortInternally = !signal;
|
|
426
|
+
let removeAbortListener;
|
|
427
|
+
if (signal && !signal.aborted) {
|
|
428
|
+
const onAbort = () => internalAbortController.abort();
|
|
429
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
430
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const response = await fetch(url, {
|
|
434
|
+
method: 'post',
|
|
435
|
+
headers: {
|
|
436
|
+
authorization: `Bearer ${apiToken}`,
|
|
437
|
+
},
|
|
438
|
+
signal: combinedSignal,
|
|
439
|
+
});
|
|
440
|
+
if (!isValidServerHeader(response)) {
|
|
441
|
+
const serverHeader = response.headers.get('Server');
|
|
442
|
+
if (!serverHeader) {
|
|
443
|
+
throw new Error('Server must be EventSourcingDB, but Server header is missing.');
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`Server must be EventSourcingDB, got Server header: '${serverHeader}'.`);
|
|
446
|
+
}
|
|
447
|
+
if (response.status !== 200) {
|
|
448
|
+
throw new Error(`Failed to read event types, got HTTP status code '${response.status}', expected '200'.`);
|
|
449
|
+
}
|
|
450
|
+
if (!response.body) {
|
|
451
|
+
throw new Error('Failed to read event types.');
|
|
452
|
+
}
|
|
453
|
+
for await (const line of readNdJsonStream(response.body, combinedSignal)) {
|
|
454
|
+
if (isStreamHeartbeat(line)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (isStreamError(line)) {
|
|
458
|
+
throw new Error(`${line.payload.error}.`);
|
|
459
|
+
}
|
|
460
|
+
if (isStreamEventType(line)) {
|
|
461
|
+
yield line.payload;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
throw new Error('Failed to read event types.');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
if (removeAbortListener) {
|
|
475
|
+
removeAbortListener();
|
|
476
|
+
}
|
|
477
|
+
if (shouldAbortInternally) {
|
|
478
|
+
internalAbortController.abort();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
})();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
export { Client };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface CloudEvent {
|
|
2
|
+
specversion: string;
|
|
3
|
+
id: string;
|
|
4
|
+
time: string;
|
|
5
|
+
source: string;
|
|
6
|
+
subject: string;
|
|
7
|
+
type: string;
|
|
8
|
+
datacontenttype: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
hash: string;
|
|
11
|
+
predecessorhash: string;
|
|
12
|
+
traceparent?: string;
|
|
13
|
+
tracestate?: string;
|
|
14
|
+
signature: string | null;
|
|
15
|
+
}
|
|
16
|
+
export type { CloudEvent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { Client } from './Client.js';
|
|
3
|
+
declare class Container {
|
|
4
|
+
#private;
|
|
5
|
+
withImageTag(tag: string): this;
|
|
6
|
+
withApiToken(token: string): this;
|
|
7
|
+
withSigningKey(): this;
|
|
8
|
+
withPort(port: number): this;
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
getHost(): string;
|
|
11
|
+
getMappedPort(): number;
|
|
12
|
+
getBaseUrl(): URL;
|
|
13
|
+
getApiToken(): string;
|
|
14
|
+
getSigningKey(): crypto.KeyObject;
|
|
15
|
+
getVerificationKey(): crypto.KeyObject;
|
|
16
|
+
isRunning(): boolean;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
getClient(): Client;
|
|
19
|
+
}
|
|
20
|
+
export { Container };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { GenericContainer, Wait } from 'testcontainers';
|
|
3
|
+
import { Client } from './Client.js';
|
|
4
|
+
class Container {
|
|
5
|
+
#imageName = 'thenativeweb/eventsourcingdb';
|
|
6
|
+
#imageTag = 'latest';
|
|
7
|
+
#internalPort = 3000;
|
|
8
|
+
#apiToken = 'secret';
|
|
9
|
+
#signingKey;
|
|
10
|
+
#container;
|
|
11
|
+
withImageTag(tag) {
|
|
12
|
+
this.#imageTag = tag;
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
withApiToken(token) {
|
|
16
|
+
this.#apiToken = token;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
withSigningKey() {
|
|
20
|
+
const { privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
21
|
+
this.#signingKey = privateKey;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
withPort(port) {
|
|
25
|
+
this.#internalPort = port;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
const command = [
|
|
30
|
+
'run',
|
|
31
|
+
'--api-token',
|
|
32
|
+
this.#apiToken,
|
|
33
|
+
'--data-directory-temporary',
|
|
34
|
+
'--http-enabled',
|
|
35
|
+
'--https-enabled=false',
|
|
36
|
+
];
|
|
37
|
+
const contents = [];
|
|
38
|
+
if (this.#signingKey !== undefined) {
|
|
39
|
+
command.push('--signing-key-file');
|
|
40
|
+
command.push('/etc/esdb/signing-key.pem');
|
|
41
|
+
contents.push({
|
|
42
|
+
content: this.#signingKey.export({ format: 'pem', type: 'pkcs8' }),
|
|
43
|
+
target: '/etc/esdb/signing-key.pem',
|
|
44
|
+
mode: 0o777,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
this.#container = await new GenericContainer(`${this.#imageName}:${this.#imageTag}`)
|
|
48
|
+
.withExposedPorts(this.#internalPort)
|
|
49
|
+
.withCommand(command)
|
|
50
|
+
.withCopyContentToContainer(contents)
|
|
51
|
+
.withWaitStrategy(Wait.forHttp('/api/v1/ping', this.#internalPort).withStartupTimeout(10_000))
|
|
52
|
+
.start();
|
|
53
|
+
}
|
|
54
|
+
getHost() {
|
|
55
|
+
if (this.#container === undefined) {
|
|
56
|
+
throw new Error('Container must be running.');
|
|
57
|
+
}
|
|
58
|
+
return this.#container.getHost();
|
|
59
|
+
}
|
|
60
|
+
getMappedPort() {
|
|
61
|
+
if (this.#container === undefined) {
|
|
62
|
+
throw new Error('Container must be running.');
|
|
63
|
+
}
|
|
64
|
+
return this.#container.getMappedPort(this.#internalPort);
|
|
65
|
+
}
|
|
66
|
+
getBaseUrl() {
|
|
67
|
+
if (this.#container === undefined) {
|
|
68
|
+
throw new Error('Container must be running.');
|
|
69
|
+
}
|
|
70
|
+
const host = this.getHost();
|
|
71
|
+
const port = this.getMappedPort();
|
|
72
|
+
return new URL(`http://${host}:${port}`);
|
|
73
|
+
}
|
|
74
|
+
getApiToken() {
|
|
75
|
+
return this.#apiToken;
|
|
76
|
+
}
|
|
77
|
+
getSigningKey() {
|
|
78
|
+
if (this.#signingKey === undefined) {
|
|
79
|
+
throw new Error('Signing key not set.');
|
|
80
|
+
}
|
|
81
|
+
return this.#signingKey;
|
|
82
|
+
}
|
|
83
|
+
getVerificationKey() {
|
|
84
|
+
if (this.#signingKey === undefined) {
|
|
85
|
+
throw new Error('Signing key not set.');
|
|
86
|
+
}
|
|
87
|
+
// @types/node >= 26.0.0 dropped the KeyObject overload from createPublicKey,
|
|
88
|
+
// even though Node accepts a (private) KeyObject at runtime to derive the public
|
|
89
|
+
// key. Remove this suppression once DefinitelyTyped restores the overload.
|
|
90
|
+
// @ts-expect-error
|
|
91
|
+
const verificationKey = crypto.createPublicKey(this.#signingKey);
|
|
92
|
+
return verificationKey;
|
|
93
|
+
}
|
|
94
|
+
isRunning() {
|
|
95
|
+
return this.#container !== undefined;
|
|
96
|
+
}
|
|
97
|
+
async stop() {
|
|
98
|
+
if (this.#container === undefined) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await this.#container.stop();
|
|
102
|
+
this.#container = undefined;
|
|
103
|
+
}
|
|
104
|
+
getClient() {
|
|
105
|
+
return new Client(this.getBaseUrl(), this.getApiToken());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export { Container };
|