@cratis/chronicle 6.49.0 → 6.51.0
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/eventSequences/AppendOptions.d.ts +10 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +6 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +28 -8
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -5
- package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
- package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -3
- package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.js +7 -4
- package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +2 -1
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
- package/dist/eventSequences/createAppendNotification.js +3 -2
- package/dist/eventSequences/createAppendNotification.js.map +1 -1
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
- package/dist/eventSequences/index.d.ts +1 -0
- package/dist/eventSequences/index.d.ts.map +1 -1
- package/dist/eventSequences/index.js +1 -0
- package/dist/eventSequences/index.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +4 -1
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.js +3 -1
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
- package/dist/events/EventContext.d.ts +7 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/InvalidNamedTag.d.ts +8 -0
- package/dist/events/InvalidNamedTag.d.ts.map +1 -0
- package/dist/events/InvalidNamedTag.js +13 -0
- package/dist/events/InvalidNamedTag.js.map +1 -0
- package/dist/events/NamedTag.d.ts +25 -0
- package/dist/events/NamedTag.d.ts.map +1 -0
- package/dist/events/NamedTag.js +39 -0
- package/dist/events/NamedTag.js.map +1 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.js +13 -0
- package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
- package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
- package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
- package/dist/events/constraints/ConstraintEventSequences.js +39 -0
- package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +24 -3
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
- package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueOptions.d.ts +15 -0
- package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
- package/dist/events/constraints/UniqueOptions.js +4 -0
- package/dist/events/constraints/UniqueOptions.js.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
- package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
- package/dist/events/constraints/index.d.ts +2 -0
- package/dist/events/constraints/index.d.ts.map +1 -1
- package/dist/events/constraints/index.js.map +1 -1
- package/dist/events/constraints/unique.d.ts +10 -0
- package/dist/events/constraints/unique.d.ts.map +1 -1
- package/dist/events/constraints/unique.js +11 -10
- package/dist/events/constraints/unique.js.map +1 -1
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeNamedTags.d.ts +13 -0
- package/dist/events/mergeNamedTags.d.ts.map +1 -0
- package/dist/events/mergeNamedTags.js +39 -0
- package/dist/events/mergeNamedTags.js.map +1 -0
- package/dist/events/toClientEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.js +3 -1
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +2 -1
- package/dist/projections/eventContextPropertyExpression.js.map +1 -1
- package/dist/testing/EventScenario.d.ts.map +1 -1
- package/dist/testing/EventScenario.js +8 -3
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/InProcessConstraints.d.ts +4 -1
- package/dist/testing/InProcessConstraints.d.ts.map +1 -1
- package/dist/testing/InProcessConstraints.js +5 -1
- package/dist/testing/InProcessConstraints.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +19 -8
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +6 -0
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +5 -0
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +12 -1
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
- package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
- package/dist/transactions/IUnitOfWork.d.ts +5 -3
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalAppendOptions.d.ts +35 -0
- package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
- package/dist/transactions/TransactionalAppendOptions.js +4 -0
- package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
- package/dist/transactions/UnitOfWork.d.ts +3 -2
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +32 -11
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
- package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
- package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
- package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
- package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
- package/dist/transactions/index.d.ts +2 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +1 -0
- package/dist/transactions/index.js.map +1 -1
- package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
- package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
- package/dist/transactions/toEventForEventSourceId.js +29 -0
- package/dist/transactions/toEventForEventSourceId.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +11 -0
- package/eventSequences/EventForEventSourceId.ts +7 -0
- package/eventSequences/EventSequence.ts +30 -8
- package/eventSequences/ITransactionalEventSequence.ts +5 -5
- package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
- package/eventSequences/TransactionalEventSequence.ts +8 -5
- package/eventSequences/createAppendNotification.ts +4 -2
- package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
- package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
- package/eventSequences/index.ts +1 -0
- package/eventSequences/prepareBatchAppend.ts +4 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/events/EventContext.ts +8 -0
- package/events/InvalidNamedTag.ts +13 -0
- package/events/NamedTag.ts +44 -0
- package/events/constraints/ConstraintBuilder.ts +16 -0
- package/events/constraints/ConstraintEventSequences.ts +41 -0
- package/events/constraints/Constraints.ts +22 -3
- package/events/constraints/IConstraintBuilder.ts +20 -0
- package/events/constraints/UniqueOptions.ts +20 -0
- package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
- package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
- package/events/constraints/index.ts +2 -0
- package/events/constraints/unique.ts +22 -2
- package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
- package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
- package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
- package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
- package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
- package/events/index.ts +3 -0
- package/events/mergeNamedTags.ts +37 -0
- package/events/toClientEventContext.ts +3 -1
- package/package.json +6 -6
- package/projections/eventContextPropertyExpression.ts +3 -2
- package/testing/EventScenario.fixtures.spec.ts +1 -1
- package/testing/EventScenario.ts +10 -4
- package/testing/InProcessConstraints.ts +5 -1
- package/testing/InProcessEventSequence.ts +19 -8
- package/testing/ReactorScenario.ts +6 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
- package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
- package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
- package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
- package/testing/singleAppendRoute.ts +13 -1
- package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
- package/transactions/IUnitOfWork.ts +5 -3
- package/transactions/TransactionalAppendOptions.ts +45 -0
- package/transactions/UnitOfWork.ts +37 -12
- package/transactions/concurrencyScopesAreEqual.ts +23 -0
- package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
- package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
- package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
- package/transactions/index.ts +2 -0
- package/transactions/toEventForEventSourceId.ts +32 -0
- package/vitest.integration.config.ts +2 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
- package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
- package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
- package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { createServer, type Server } from 'node:http';
|
|
5
|
+
import type { AddressInfo } from 'node:net';
|
|
6
|
+
import type { ReceivedWebhookRequest } from './ReceivedWebhookRequest.fixture.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The host name the kernel container uses to reach this process. Docker Desktop resolves
|
|
10
|
+
* host.docker.internal natively; on Linux (CI) the kernel container maps it to the host gateway
|
|
11
|
+
* (--add-host=host.docker.internal:host-gateway, see .github/workflows/build.yml).
|
|
12
|
+
* Override with CHRONICLE_INTEGRATION_WEBHOOK_HOST when the kernel reaches this process differently.
|
|
13
|
+
*/
|
|
14
|
+
const receiverHost = process.env.CHRONICLE_INTEGRATION_WEBHOOK_HOST ?? 'host.docker.internal';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A local HTTP endpoint the kernel delivers webhook requests to. It records every request and
|
|
18
|
+
* answers each with a fixed status code.
|
|
19
|
+
*/
|
|
20
|
+
export class WebhookReceiver {
|
|
21
|
+
private readonly _requests: ReceivedWebhookRequest[] = [];
|
|
22
|
+
private _server: Server | undefined;
|
|
23
|
+
private _port = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes a new instance of the {@link WebhookReceiver} class.
|
|
27
|
+
* @param _statusCode - The status code to answer every request with.
|
|
28
|
+
*/
|
|
29
|
+
constructor(private readonly _statusCode: number) {}
|
|
30
|
+
|
|
31
|
+
/** Gets the requests received so far, in arrival order. */
|
|
32
|
+
get requests(): ReadonlyArray<ReceivedWebhookRequest> {
|
|
33
|
+
return this._requests;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Starts listening on all interfaces on a free port, so a kernel container can reach it.
|
|
38
|
+
*/
|
|
39
|
+
async start(): Promise<void> {
|
|
40
|
+
const server = createServer((request, response) => {
|
|
41
|
+
const chunks: Buffer[] = [];
|
|
42
|
+
request.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
43
|
+
request.on('end', () => {
|
|
44
|
+
this._requests.push({
|
|
45
|
+
method: request.method ?? '',
|
|
46
|
+
path: request.url ?? '',
|
|
47
|
+
headers: request.headers,
|
|
48
|
+
body: Buffer.concat(chunks).toString('utf8')
|
|
49
|
+
});
|
|
50
|
+
response.statusCode = this._statusCode;
|
|
51
|
+
response.end();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
await new Promise<void>((resolve, reject) => {
|
|
55
|
+
server.once('error', reject);
|
|
56
|
+
server.listen(0, '0.0.0.0', () => resolve());
|
|
57
|
+
});
|
|
58
|
+
this._server = server;
|
|
59
|
+
this._port = (server.address() as AddressInfo).port;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the URL the kernel should deliver to for the given path.
|
|
64
|
+
* @param path - The path, starting with a slash.
|
|
65
|
+
* @returns The URL as seen from the kernel.
|
|
66
|
+
*/
|
|
67
|
+
urlFor(path: string): string {
|
|
68
|
+
return `http://${receiverHost}:${this._port}${path}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Waits until at least the given number of requests have arrived.
|
|
73
|
+
* @param count - The number of requests to wait for.
|
|
74
|
+
* @param timeoutMs - How long to wait before failing.
|
|
75
|
+
* @returns The requests received.
|
|
76
|
+
*/
|
|
77
|
+
async waitForRequests(count: number, timeoutMs = 30_000): Promise<ReadonlyArray<ReceivedWebhookRequest>> {
|
|
78
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
|
+
while (this._requests.length < count) {
|
|
80
|
+
if (Date.now() > deadline) {
|
|
81
|
+
throw new Error(`Timed out waiting for ${count} webhook request(s); received ${this._requests.length}. ` +
|
|
82
|
+
`Is ${receiverHost} reachable from the kernel?`);
|
|
83
|
+
}
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
85
|
+
}
|
|
86
|
+
return this._requests;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stops listening.
|
|
91
|
+
*/
|
|
92
|
+
async stop(): Promise<void> {
|
|
93
|
+
const server = this._server;
|
|
94
|
+
if (!server) return;
|
|
95
|
+
this._server = undefined;
|
|
96
|
+
server.closeAllConnections();
|
|
97
|
+
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
8
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
9
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
10
|
+
import { eventType } from '../../../events/index.js';
|
|
11
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
12
|
+
import type { FailedPartition, ObserverInformation } from '../../../observation/index.js';
|
|
13
|
+
import { WebhookReceiver } from './given/WebhookReceiver.fixture.js';
|
|
14
|
+
|
|
15
|
+
chai.should();
|
|
16
|
+
|
|
17
|
+
// Runs against a real Chronicle kernel; skipped locally unless the connection string is set (see vitest.integration.config.ts).
|
|
18
|
+
// In CI it never skips, so a missing connection string or unreachable kernel fails instead of passing vacuously.
|
|
19
|
+
// The kernel must reach this process over HTTP; see WebhookReceiver for the address it uses.
|
|
20
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
21
|
+
|
|
22
|
+
@eventType()
|
|
23
|
+
class ParcelLost {
|
|
24
|
+
@field(String) parcelId = '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const artifacts: IClientArtifactsProvider = {
|
|
28
|
+
eventTypes: [ParcelLost],
|
|
29
|
+
readModels: [],
|
|
30
|
+
reactors: [],
|
|
31
|
+
reducers: [],
|
|
32
|
+
seeders: [],
|
|
33
|
+
constraints: [],
|
|
34
|
+
projections: [],
|
|
35
|
+
webhooks: [],
|
|
36
|
+
eventTypeMigrations: [],
|
|
37
|
+
globalForHandlers: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function eventually<T>(read: () => Promise<T>, accept: (value: T) => boolean, timeoutMs = 30_000): Promise<T> {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
let value = await read();
|
|
43
|
+
while (!accept(value)) {
|
|
44
|
+
if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
46
|
+
value = await read();
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe.skipIf(!connectionString && !process.env.CI)('when delivering events to a failing webhook against a kernel', () => {
|
|
52
|
+
const webhookId = randomUUID();
|
|
53
|
+
const eventSourceId = randomUUID();
|
|
54
|
+
const receiver = new WebhookReceiver(500);
|
|
55
|
+
let client: ChronicleClient;
|
|
56
|
+
let store: IEventStore;
|
|
57
|
+
let appendedSequenceNumber: bigint;
|
|
58
|
+
let failedPartition: FailedPartition;
|
|
59
|
+
let observer: ObserverInformation | undefined;
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
await receiver.start();
|
|
63
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
64
|
+
discoveryPatterns: [],
|
|
65
|
+
clientArtifactsProvider: artifacts
|
|
66
|
+
}));
|
|
67
|
+
store = await client.getEventStore(`Webhooks${randomUUID().replaceAll('-', '').slice(0, 12)}`);
|
|
68
|
+
|
|
69
|
+
await store.webhooks.register(webhookId, receiver.urlFor('/parcels'), _ => _.withEventType(ParcelLost));
|
|
70
|
+
|
|
71
|
+
const parcel = new ParcelLost();
|
|
72
|
+
parcel.parcelId = randomUUID();
|
|
73
|
+
appendedSequenceNumber = (await store.eventLog.append(eventSourceId, parcel)).sequenceNumber.value;
|
|
74
|
+
|
|
75
|
+
await receiver.waitForRequests(1);
|
|
76
|
+
const failed = await eventually(
|
|
77
|
+
() => store.failedPartitions.getFailedPartitionsFor(webhookId),
|
|
78
|
+
_ => _.some(partition => partition.partition === eventSourceId));
|
|
79
|
+
failedPartition = failed.find(_ => _.partition === eventSourceId)!;
|
|
80
|
+
observer = (await store.observers.getAll()).find(_ => _.id === webhookId);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterAll(async () => {
|
|
84
|
+
try {
|
|
85
|
+
await store?.webhooks.remove(webhookId);
|
|
86
|
+
} finally {
|
|
87
|
+
try {
|
|
88
|
+
await receiver.stop();
|
|
89
|
+
} finally {
|
|
90
|
+
client?.dispose();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should have attempted the delivery', () => receiver.requests.length.should.be.greaterThan(0));
|
|
96
|
+
it('should record the attempt for the appended event', () => failedPartition.attempts[0].sequenceNumber.value.should.equal(appendedSequenceNumber));
|
|
97
|
+
// The kernel retries the failing status with backoff inside the delivery, so the recorded reason is usually the
|
|
98
|
+
// subscriber timeout rather than the status code; either way it must carry a reason.
|
|
99
|
+
it('should record why the attempt failed', () => failedPartition.attempts[0].messages.should.not.be.empty);
|
|
100
|
+
it('should not record the event as handled', () => observer!.lastHandledEventSequenceNumber.value.should.not.equal(appendedSequenceNumber));
|
|
101
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
8
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
9
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
10
|
+
import { eventType } from '../../../events/index.js';
|
|
11
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
12
|
+
import type { FailedPartition } from '../../../observation/index.js';
|
|
13
|
+
import type { ReceivedWebhookRequest } from './given/ReceivedWebhookRequest.fixture.js';
|
|
14
|
+
import { WebhookReceiver } from './given/WebhookReceiver.fixture.js';
|
|
15
|
+
|
|
16
|
+
chai.should();
|
|
17
|
+
|
|
18
|
+
// Runs against a real Chronicle kernel; skipped locally unless the connection string is set (see vitest.integration.config.ts).
|
|
19
|
+
// In CI it never skips, so a missing connection string or unreachable kernel fails instead of passing vacuously.
|
|
20
|
+
// The kernel must reach this process over HTTP; see WebhookReceiver for the address it uses.
|
|
21
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
22
|
+
|
|
23
|
+
@eventType()
|
|
24
|
+
class ParcelDelivered {
|
|
25
|
+
@field(String) parcelId = '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const artifacts: IClientArtifactsProvider = {
|
|
29
|
+
eventTypes: [ParcelDelivered],
|
|
30
|
+
readModels: [],
|
|
31
|
+
reactors: [],
|
|
32
|
+
reducers: [],
|
|
33
|
+
seeders: [],
|
|
34
|
+
constraints: [],
|
|
35
|
+
projections: [],
|
|
36
|
+
webhooks: [],
|
|
37
|
+
eventTypeMigrations: [],
|
|
38
|
+
globalForHandlers: []
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface DeliveredEvent {
|
|
42
|
+
readonly context: {
|
|
43
|
+
readonly eventType: { readonly id: string; readonly generation: number };
|
|
44
|
+
readonly eventSourceId: string;
|
|
45
|
+
readonly sequenceNumber: number;
|
|
46
|
+
};
|
|
47
|
+
readonly content: { readonly parcelId: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DeliveredBody {
|
|
51
|
+
readonly partition: string;
|
|
52
|
+
readonly events: ReadonlyArray<DeliveredEvent>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function eventually<T>(read: () => Promise<T>, accept: (value: T) => boolean, timeoutMs = 30_000): Promise<T> {
|
|
56
|
+
const deadline = Date.now() + timeoutMs;
|
|
57
|
+
let value = await read();
|
|
58
|
+
while (!accept(value)) {
|
|
59
|
+
if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
61
|
+
value = await read();
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe.skipIf(!connectionString && !process.env.CI)('when delivering events to a webhook against a kernel', () => {
|
|
67
|
+
const webhookId = randomUUID();
|
|
68
|
+
const eventSourceId = randomUUID();
|
|
69
|
+
const parcelId = randomUUID();
|
|
70
|
+
const receiver = new WebhookReceiver(200);
|
|
71
|
+
let client: ChronicleClient;
|
|
72
|
+
let store: IEventStore;
|
|
73
|
+
let appendedSequenceNumber: bigint;
|
|
74
|
+
let request: ReceivedWebhookRequest;
|
|
75
|
+
let body: DeliveredBody;
|
|
76
|
+
let failedPartitions: FailedPartition[];
|
|
77
|
+
|
|
78
|
+
beforeAll(async () => {
|
|
79
|
+
await receiver.start();
|
|
80
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
81
|
+
discoveryPatterns: [],
|
|
82
|
+
clientArtifactsProvider: artifacts
|
|
83
|
+
}));
|
|
84
|
+
store = await client.getEventStore(`Webhooks${randomUUID().replaceAll('-', '').slice(0, 12)}`);
|
|
85
|
+
|
|
86
|
+
await store.webhooks.register(webhookId, receiver.urlFor('/parcels'), _ => _
|
|
87
|
+
.withEventType(ParcelDelivered)
|
|
88
|
+
.withHeader('X-Source', 'specs'));
|
|
89
|
+
|
|
90
|
+
const parcel = new ParcelDelivered();
|
|
91
|
+
parcel.parcelId = parcelId;
|
|
92
|
+
const result = await store.eventLog.append(eventSourceId, parcel);
|
|
93
|
+
appendedSequenceNumber = result.sequenceNumber.value;
|
|
94
|
+
|
|
95
|
+
request = (await receiver.waitForRequests(1))[0];
|
|
96
|
+
body = JSON.parse(request.body) as DeliveredBody;
|
|
97
|
+
|
|
98
|
+
// Once the kernel records the event as handled it has acted on the receiver's answer.
|
|
99
|
+
await eventually(
|
|
100
|
+
() => store.observers.getAll(),
|
|
101
|
+
_ => _.some(observer => observer.id === webhookId && observer.lastHandledEventSequenceNumber.value === appendedSequenceNumber));
|
|
102
|
+
failedPartitions = await store.failedPartitions.getFailedPartitionsFor(webhookId);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterAll(async () => {
|
|
106
|
+
try {
|
|
107
|
+
await store?.webhooks.remove(webhookId);
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
await receiver.stop();
|
|
111
|
+
} finally {
|
|
112
|
+
client?.dispose();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should post to the target path', () => `${request.method} ${request.path}`.should.equal('POST /parcels'));
|
|
118
|
+
it('should send json', () => request.headers['content-type']!.should.match(/^application\/json/));
|
|
119
|
+
// Kernels up to at least 19.30.0 drop basic, bearer and OAuth authorization for webhooks outside the System event store
|
|
120
|
+
// while reporting the registration as successful, so no Authorization header is ever sent (Cratis/Chronicle#4567).
|
|
121
|
+
it.todo('should authorize with the configured credentials once Cratis/Chronicle#4567 is fixed');
|
|
122
|
+
it('should send the configured header', () => request.headers['x-source']!.should.equal('specs'));
|
|
123
|
+
it('should deliver the event source as the partition', () => body.partition.should.equal(eventSourceId));
|
|
124
|
+
it('should deliver the appended event only', () => body.events.length.should.equal(1));
|
|
125
|
+
it('should deliver the event content', () => body.events[0].content.parcelId.should.equal(parcelId));
|
|
126
|
+
it('should deliver the event type', () => body.events[0].context.eventType.id.should.equal('ParcelDelivered'));
|
|
127
|
+
it('should deliver the event source id', () => body.events[0].context.eventSourceId.should.equal(eventSourceId));
|
|
128
|
+
it('should deliver the sequence number', () => BigInt(body.events[0].context.sequenceNumber).should.equal(appendedSequenceNumber));
|
|
129
|
+
it('should not report the webhook as failed', () => failedPartitions.should.be.empty);
|
|
130
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import type { WebhookDefinition } from '@cratis/chronicle.contracts';
|
|
7
|
+
import { field } from '@cratis/fundamentals';
|
|
8
|
+
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
9
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
10
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
11
|
+
import { eventType } from '../../../events/index.js';
|
|
12
|
+
import type { IEventStore } from '../../../IEventStore.js';
|
|
13
|
+
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
// Runs against a real Chronicle kernel; skipped locally unless the connection string is set (see vitest.integration.config.ts).
|
|
17
|
+
// In CI it never skips, so a missing connection string or unreachable kernel fails instead of passing vacuously.
|
|
18
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
19
|
+
|
|
20
|
+
@eventType()
|
|
21
|
+
class ParcelReturned {
|
|
22
|
+
@field(String) parcelId = '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const artifacts: IClientArtifactsProvider = {
|
|
26
|
+
eventTypes: [ParcelReturned],
|
|
27
|
+
readModels: [],
|
|
28
|
+
reactors: [],
|
|
29
|
+
reducers: [],
|
|
30
|
+
seeders: [],
|
|
31
|
+
constraints: [],
|
|
32
|
+
projections: [],
|
|
33
|
+
webhooks: [],
|
|
34
|
+
eventTypeMigrations: [],
|
|
35
|
+
globalForHandlers: []
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async function eventually<T>(read: () => Promise<T>, accept: (value: T) => boolean, timeoutMs = 20_000): Promise<T> {
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
let value = await read();
|
|
41
|
+
while (!accept(value)) {
|
|
42
|
+
if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
44
|
+
value = await read();
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe.skipIf(!connectionString && !process.env.CI)('when registering a webhook that is not replayable and not active against a kernel', () => {
|
|
50
|
+
const defaultWebhookId = randomUUID();
|
|
51
|
+
const restrictedWebhookId = randomUUID();
|
|
52
|
+
const targetUrl = 'http://localhost:9/parcels';
|
|
53
|
+
let client: ChronicleClient;
|
|
54
|
+
let store: IEventStore;
|
|
55
|
+
let defaultWebhook: WebhookDefinition;
|
|
56
|
+
let restrictedWebhook: WebhookDefinition;
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
60
|
+
discoveryPatterns: [],
|
|
61
|
+
clientArtifactsProvider: artifacts
|
|
62
|
+
}));
|
|
63
|
+
store = await client.getEventStore(`Webhooks${randomUUID().replaceAll('-', '').slice(0, 12)}`);
|
|
64
|
+
|
|
65
|
+
// A webhook with the defaults proves the read-back distinguishes true from false, so a false
|
|
66
|
+
// below cannot be an artifact of how an absent field decodes.
|
|
67
|
+
await store.webhooks.register(defaultWebhookId, targetUrl, _ => _.withEventType(ParcelReturned));
|
|
68
|
+
await store.webhooks.register(restrictedWebhookId, targetUrl, _ => _
|
|
69
|
+
.withEventType(ParcelReturned)
|
|
70
|
+
.notReplayable()
|
|
71
|
+
.notActive());
|
|
72
|
+
|
|
73
|
+
const webhooks = await eventually(
|
|
74
|
+
() => store.webhooks.getWebhooks(),
|
|
75
|
+
_ => [defaultWebhookId, restrictedWebhookId].every(id => _.some(webhook => webhook.Identifier === id)));
|
|
76
|
+
defaultWebhook = webhooks.find(_ => _.Identifier === defaultWebhookId)!;
|
|
77
|
+
restrictedWebhook = webhooks.find(_ => _.Identifier === restrictedWebhookId)!;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(async () => {
|
|
81
|
+
const failures: unknown[] = [];
|
|
82
|
+
for (const id of [defaultWebhookId, restrictedWebhookId]) {
|
|
83
|
+
try {
|
|
84
|
+
await store?.webhooks.remove(id);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
failures.push(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
client?.dispose();
|
|
90
|
+
if (failures.length > 0) throw failures[0];
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should keep a default webhook replayable', () => defaultWebhook.IsReplayable.should.be.true);
|
|
94
|
+
it('should keep a default webhook active', () => defaultWebhook.IsActive.should.be.true);
|
|
95
|
+
it('should keep the webhook not replayable', () => restrictedWebhook.IsReplayable.should.be.false);
|
|
96
|
+
// The kernel records IsActive=false in WebhookAdded, but its MongoDB storage converter drops IsActive when reading
|
|
97
|
+
// definitions back (Storage.MongoDB/Observation/Webhooks/WebhookDefinitionConverters.cs, ToKernel), so every
|
|
98
|
+
// kernel up to at least 19.30.0 lists the webhook as active (Cratis/Chronicle#4581). The client sending
|
|
99
|
+
// IsActive=false is covered by the unit spec when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts.
|
|
100
|
+
it.todo('should keep the webhook not active once the kernel reads IsActive back from storage');
|
|
101
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { AddWebhooks } from '@cratis/chronicle.contracts';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
8
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
9
|
+
import { eventType } from '../../events/index.js';
|
|
10
|
+
import type { IEventTypes } from '../../events/IEventTypes.js';
|
|
11
|
+
import { Webhooks } from '../Webhooks.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
|
|
15
|
+
@eventType()
|
|
16
|
+
class ParcelReturned {
|
|
17
|
+
@field(String) parcelId = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('when registering a webhook that is not replayable and not active', () => {
|
|
21
|
+
let sent: AddWebhooks | undefined;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
sent = undefined;
|
|
25
|
+
const connection = {
|
|
26
|
+
webhooks: {
|
|
27
|
+
addWebhooks: async (request: AddWebhooks) => {
|
|
28
|
+
sent = request;
|
|
29
|
+
return { IsAuthorized: true, ValidationResults: [], ExceptionMessages: [] };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} as unknown as ChronicleConnection;
|
|
33
|
+
const webhooks = new Webhooks('store', connection, { all: [] } as unknown as IEventTypes, {} as IClientArtifactsProvider);
|
|
34
|
+
|
|
35
|
+
await webhooks.register('parcels', 'http://localhost:9/parcels', _ => _
|
|
36
|
+
.withEventType(ParcelReturned)
|
|
37
|
+
.notReplayable()
|
|
38
|
+
.notActive());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should send the webhook as not active', () => sent!.Webhooks[0].IsActive.should.be.false);
|
|
42
|
+
it('should send the webhook as not replayable', () => sent!.Webhooks[0].IsReplayable.should.be.false);
|
|
43
|
+
});
|