@cratis/chronicle 6.27.0 → 6.28.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/connection/ChronicleConnection.auth.spec.ts +15 -11
- package/dist/projections/modelBound/childrenFrom.d.ts +19 -0
- package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenFrom.js +14 -13
- package/dist/projections/modelBound/childrenFrom.js.map +1 -1
- package/dist/projections/modelBound/index.d.ts +1 -1
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/projections/Projections.childrenFromOptions.spec.ts +118 -0
- package/projections/modelBound/childrenFrom.ts +41 -5
- package/projections/modelBound/index.ts +1 -1
- package/testing/ReadModelScenario.childrenFrom.spec.ts +35 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest';
|
|
5
5
|
import { createServer, ServerError, type ServiceImplementation } from 'nice-grpc';
|
|
6
6
|
import { ConnectionServiceDefinition, EventStoresDefinition, ServerDefinition } from '@cratis/chronicle.contracts';
|
|
7
7
|
import { status } from '@grpc/grpc-js';
|
|
@@ -233,20 +233,24 @@ describe('ChronicleConnection authentication', () => {
|
|
|
233
233
|
|
|
234
234
|
it('keeps retrying proxy 403 responses without an OAuth error code', async () => {
|
|
235
235
|
const random = vi.spyOn(Math, 'random').mockReturnValue(0);
|
|
236
|
-
|
|
236
|
+
// Restore even when the test times out, so a hang cannot leak the stub into later tests.
|
|
237
|
+
onTestFinished(() => random.mockRestore());
|
|
238
|
+
let fourthCheck!: () => void;
|
|
239
|
+
const reachedFourthCheck = new Promise<void>(resolve => fourthCheck = resolve);
|
|
240
|
+
const checks = vi.fn(() => { if (checks.mock.calls.length === 4) fourthCheck(); });
|
|
237
241
|
const port = await listenAuthenticated(checks);
|
|
238
242
|
fetchToken.mockRejectedValue(new OAuthTokenHttpError(403, '<html>blocked</html>'));
|
|
239
243
|
const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
|
|
240
244
|
clients.push(client);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
let settled = false;
|
|
246
|
+
const pending = client.getEventStores().then(() => undefined, error => error as Error).finally(() => settled = true);
|
|
247
|
+
// Wait for the fourth attempt itself rather than a wall-clock window; a credential
|
|
248
|
+
// rejection would stop after three attempts and settle the call instead.
|
|
249
|
+
await Promise.race([reachedFourthCheck, pending]);
|
|
250
|
+
expect(settled).toBe(false);
|
|
251
|
+
expect(checks.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
252
|
+
client.dispose();
|
|
253
|
+
expect((await pending).message).toMatch(/disposed/);
|
|
250
254
|
}, 15000);
|
|
251
255
|
|
|
252
256
|
it('rejects an API key after three unauthenticated kernel responses', async () => {
|
|
@@ -13,6 +13,25 @@ export interface ChildrenFromMetadata {
|
|
|
13
13
|
/** The event property name used as the parent key. Defaults to the event source identifier. */
|
|
14
14
|
readonly parentKey?: string;
|
|
15
15
|
}
|
|
16
|
+
/** Options for the options form of the childrenFrom property decorator. */
|
|
17
|
+
export interface ChildrenFromOptions {
|
|
18
|
+
/** The child type, for TypeScript runtimes without emitted generic metadata. */
|
|
19
|
+
readonly childType?: Function;
|
|
20
|
+
/** The event property name used as the key for children. Defaults to the event source identifier. */
|
|
21
|
+
readonly key?: string;
|
|
22
|
+
/** The child model property name used to uniquely identify instances in the collection. */
|
|
23
|
+
readonly identifiedBy?: string;
|
|
24
|
+
/** The event property name used as the parent key. Defaults to the event source identifier. */
|
|
25
|
+
readonly parentKey?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Property decorator that configures a children collection sub-projection from an event type.
|
|
29
|
+
* Use this form to name the child type and an explicit child key together.
|
|
30
|
+
* @param eventType - The event constructor.
|
|
31
|
+
* @param options - The child type, key, identifiedBy and parentKey.
|
|
32
|
+
* @returns A property decorator.
|
|
33
|
+
*/
|
|
34
|
+
export declare function childrenFrom(eventType: Function, options: ChildrenFromOptions): ChroniclePropertyDecorator;
|
|
16
35
|
/**
|
|
17
36
|
* Property decorator that configures a children collection sub-projection from an event type.
|
|
18
37
|
* @param eventType - The event constructor.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"childrenFrom.d.ts","sourceRoot":"","sources":["../../../projections/modelBound/childrenFrom.ts"],"names":[],"mappings":"AAGA,OAAO,kBAAkB,CAAC;AAG1B,OAAO,EAAE,0BAA0B,EAAuB,MAAM,0CAA0C,CAAC;AAE3G,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB;IACjC,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID;;;;;;;GAOG;AACH,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"childrenFrom.d.ts","sourceRoot":"","sources":["../../../projections/modelBound/childrenFrom.ts"],"names":[],"mappings":"AAGA,OAAO,kBAAkB,CAAC;AAG1B,OAAO,EAAE,0BAA0B,EAAuB,MAAM,0CAA0C,CAAC;AAE3G,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB;IACjC,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;IAC9B,qGAAqG;IACrG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,GAAG,0BAA0B,CAAC;AAC5G;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,0BAA0B,CAAC;AAkClJ;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAEnG"}
|
|
@@ -5,26 +5,27 @@ import { TypeIntrospector } from '../../types/index.js';
|
|
|
5
5
|
import { decorateModelBoundProperty } from '../../types/modelBoundProperty.js';
|
|
6
6
|
import { getPropertyMetadata } from '../../types/propertyDecoratorMetadata.js';
|
|
7
7
|
const METADATA_KEY = 'chronicle:projection:childrenFrom';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @param eventType - The event constructor.
|
|
11
|
-
* @param key - Optional event property name to use as the key for children, or the child constructor.
|
|
12
|
-
* @param identifiedBy - Optional child model property name used to identify instances.
|
|
13
|
-
* @param parentKey - Optional event property name used as the parent key.
|
|
14
|
-
* @returns A property decorator.
|
|
15
|
-
*/
|
|
16
|
-
export function childrenFrom(eventType, key, identifiedBy, parentKey) {
|
|
8
|
+
export function childrenFrom(eventType, keyOrOptions, identifiedBy, parentKey) {
|
|
9
|
+
const metadata = toMetadata(eventType, keyOrOptions, identifiedBy, parentKey);
|
|
17
10
|
return decorateModelBoundProperty((target, propertyKey) => {
|
|
18
11
|
const propKey = propertyKey.toString();
|
|
19
12
|
TypeIntrospector.trackProperty(target.constructor, propKey);
|
|
20
13
|
const existing = Reflect.getMetadata(METADATA_KEY, target, propKey) ?? [];
|
|
21
|
-
const metadata = {
|
|
22
|
-
eventType, key: typeof key === 'string' ? key : undefined,
|
|
23
|
-
childType: typeof key === 'function' ? key : undefined, identifiedBy, parentKey
|
|
24
|
-
};
|
|
25
14
|
Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, propKey);
|
|
26
15
|
});
|
|
27
16
|
}
|
|
17
|
+
function toMetadata(eventType, keyOrOptions, identifiedBy, parentKey) {
|
|
18
|
+
if (typeof keyOrOptions === 'object' && keyOrOptions !== null) {
|
|
19
|
+
return {
|
|
20
|
+
eventType, childType: keyOrOptions.childType, key: keyOrOptions.key,
|
|
21
|
+
identifiedBy: keyOrOptions.identifiedBy, parentKey: keyOrOptions.parentKey
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
eventType, key: typeof keyOrOptions === 'string' ? keyOrOptions : undefined,
|
|
26
|
+
childType: typeof keyOrOptions === 'function' ? keyOrOptions : undefined, identifiedBy, parentKey
|
|
27
|
+
};
|
|
28
|
+
}
|
|
28
29
|
/**
|
|
29
30
|
* Retrieves all childrenFrom metadata stored on the given property.
|
|
30
31
|
* @param target - The class prototype.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"childrenFrom.js","sourceRoot":"","sources":["../../../projections/modelBound/childrenFrom.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAA8B,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"childrenFrom.js","sourceRoot":"","sources":["../../../projections/modelBound/childrenFrom.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAA8B,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AA4B3G,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAmBzD,MAAM,UAAU,YAAY,CACxB,SAAmB,EACnB,YAAsD,EACtD,YAAqB,EACrB,SAAkB;IAElB,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;IAC9E,OAAO,0BAA0B,CAAC,CAAC,MAAc,EAAE,WAA4B,EAAE,EAAE;QAC/E,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,CAAC;QACvC,gBAAgB,CAAC,aAAa,CAAE,MAAoC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAC3F,MAAM,QAAQ,GAA2B,OAAO,CAAC,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;QAClG,OAAO,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC,GAAG,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,UAAU,CACf,SAAmB,EACnB,YAAiE,EACjE,YAAgC,EAChC,SAA6B;IAE7B,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC5D,OAAO;YACH,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC,SAAS,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG;YACnE,YAAY,EAAE,YAAY,CAAC,YAAY,EAAE,SAAS,EAAE,YAAY,CAAC,SAAS;SAC7E,CAAC;IACN,CAAC;IACD,OAAO;QACH,SAAS,EAAE,GAAG,EAAE,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;QAC3E,SAAS,EAAE,OAAO,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS;KACpG,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,WAAmB;IACvE,OAAO,mBAAmB,CAAyB,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;AAChG,CAAC"}
|
|
@@ -18,7 +18,7 @@ export type { DecrementMetadata } from './decrement.js';
|
|
|
18
18
|
export { count, getCountMetadata } from './count.js';
|
|
19
19
|
export type { CountMetadata } from './count.js';
|
|
20
20
|
export { childrenFrom, getChildrenFromMetadata } from './childrenFrom.js';
|
|
21
|
-
export type { ChildrenFromMetadata } from './childrenFrom.js';
|
|
21
|
+
export type { ChildrenFromMetadata, ChildrenFromOptions } from './childrenFrom.js';
|
|
22
22
|
export { nested, isNested } from './nested.js';
|
|
23
23
|
export { clearWith, getClearWithClassMetadata, getClearWithPropertyMetadata } from './clearWith.js';
|
|
24
24
|
export type { ClearWithMetadata } from './clearWith.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../projections/modelBound/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACvF,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../projections/modelBound/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
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 'reflect-metadata';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
7
|
+
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
8
|
+
import { ChronicleConnection } from '../connection/index.js';
|
|
9
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
10
|
+
import { childrenFrom } from './modelBound/childrenFrom.js';
|
|
11
|
+
import { fromEvent } from './modelBound/fromEvent.js';
|
|
12
|
+
import { Projections } from './Projections.js';
|
|
13
|
+
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
class KeyedItemAdded {
|
|
17
|
+
itemId = '';
|
|
18
|
+
orderId = '';
|
|
19
|
+
name = '';
|
|
20
|
+
}
|
|
21
|
+
eventType()(KeyedItemAdded);
|
|
22
|
+
|
|
23
|
+
class KeyedOrderCreated {}
|
|
24
|
+
eventType()(KeyedOrderCreated);
|
|
25
|
+
|
|
26
|
+
// No @field metadata on any collection: the child type comes only from the options.
|
|
27
|
+
class KeyedItem {
|
|
28
|
+
id = '';
|
|
29
|
+
sku = '';
|
|
30
|
+
name = '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class KeyedOrder {
|
|
34
|
+
byKey: KeyedItem[] = [];
|
|
35
|
+
byIdentifiedBy: KeyedItem[] = [];
|
|
36
|
+
byParentKey: KeyedItem[] = [];
|
|
37
|
+
byKeyAndIdentifiedBy: KeyedItem[] = [];
|
|
38
|
+
byKeyAndParentKey: KeyedItem[] = [];
|
|
39
|
+
byAll: KeyedItem[] = [];
|
|
40
|
+
byFieldMetadata: KeyedItem[] = [];
|
|
41
|
+
}
|
|
42
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, key: 'itemId' })(KeyedOrder.prototype, 'byKey');
|
|
43
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, identifiedBy: 'sku' })(KeyedOrder.prototype, 'byIdentifiedBy');
|
|
44
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, parentKey: 'orderId' })(KeyedOrder.prototype, 'byParentKey');
|
|
45
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, key: 'itemId', identifiedBy: 'sku' })(KeyedOrder.prototype, 'byKeyAndIdentifiedBy');
|
|
46
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, key: 'itemId', parentKey: 'orderId' })(KeyedOrder.prototype, 'byKeyAndParentKey');
|
|
47
|
+
childrenFrom(KeyedItemAdded, { childType: KeyedItem, key: 'itemId', identifiedBy: 'sku', parentKey: 'orderId' })(KeyedOrder.prototype, 'byAll');
|
|
48
|
+
childrenFrom(KeyedItemAdded, { key: 'itemId' })(KeyedOrder.prototype, 'byFieldMetadata');
|
|
49
|
+
field(Array, { enumerable: true, genericArguments: [KeyedItem] })(KeyedOrder.prototype, 'byFieldMetadata');
|
|
50
|
+
fromEvent(KeyedOrderCreated)(KeyedOrder);
|
|
51
|
+
|
|
52
|
+
interface ChildrenDefinition {
|
|
53
|
+
IdentifiedBy: string;
|
|
54
|
+
From: Array<{ Key: { Id: string }; Value: { Key: string; ParentKey: string; Properties: Record<string, string> } }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function registeredChildren(): Promise<Record<string, ChildrenDefinition>> {
|
|
58
|
+
const register = vi.fn().mockResolvedValue(undefined);
|
|
59
|
+
const connection = {
|
|
60
|
+
readModels: { registerMany: vi.fn().mockResolvedValue(undefined) },
|
|
61
|
+
projections: { register }
|
|
62
|
+
} as unknown as ChronicleConnection;
|
|
63
|
+
const artifacts = {
|
|
64
|
+
eventTypes: [], readModels: [KeyedOrder], reactors: [], reducers: [], seeders: [], constraints: [],
|
|
65
|
+
projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
66
|
+
} as unknown as IClientArtifactsProvider;
|
|
67
|
+
await new Projections('test-store', 'test-namespace', connection, artifacts, 'test-sink').register();
|
|
68
|
+
return register.mock.calls[0][0].Projections[0].Children;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function creation(children: Record<string, ChildrenDefinition>, property: string) {
|
|
72
|
+
const child = children[property];
|
|
73
|
+
const entry = child.From.find(candidate => candidate.Key.Id === 'KeyedItemAdded')!;
|
|
74
|
+
return { identifiedBy: child.IdentifiedBy, key: entry.Value.Key, parentKey: entry.Value.ParentKey, properties: entry.Value.Properties };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('when registering childrenFrom with the options form', () => {
|
|
78
|
+
it('should key children by the event property and map the discovered identifier from it', async () => {
|
|
79
|
+
creation(await registeredChildren(), 'byKey').should.deep.equal({
|
|
80
|
+
identifiedBy: 'id', key: 'itemId', parentKey: '$eventSourceId', properties: { id: 'itemId' }
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should use an explicit identifiedBy with the default event source key', async () => {
|
|
85
|
+
creation(await registeredChildren(), 'byIdentifiedBy').should.deep.equal({
|
|
86
|
+
identifiedBy: 'sku', key: '$eventSourceId', parentKey: '$eventSourceId', properties: { sku: '$eventContext(EventSourceId)' }
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should use an explicit parent key with the default child key', async () => {
|
|
91
|
+
creation(await registeredChildren(), 'byParentKey').should.deep.equal({
|
|
92
|
+
identifiedBy: 'id', key: '$eventSourceId', parentKey: 'orderId', properties: { id: '$eventContext(EventSourceId)' }
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should map an explicit identifiedBy from an explicit key', async () => {
|
|
97
|
+
creation(await registeredChildren(), 'byKeyAndIdentifiedBy').should.deep.equal({
|
|
98
|
+
identifiedBy: 'sku', key: 'itemId', parentKey: '$eventSourceId', properties: { sku: 'itemId' }
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should combine an explicit key and parent key', async () => {
|
|
103
|
+
creation(await registeredChildren(), 'byKeyAndParentKey').should.deep.equal({
|
|
104
|
+
identifiedBy: 'id', key: 'itemId', parentKey: 'orderId', properties: { id: 'itemId' }
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should combine key, identifiedBy and parent key', async () => {
|
|
109
|
+
creation(await registeredChildren(), 'byAll').should.deep.equal({
|
|
110
|
+
identifiedBy: 'sku', key: 'itemId', parentKey: 'orderId', properties: { sku: 'itemId' }
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should emit the same definition when the child type comes from field metadata', async () => {
|
|
115
|
+
const children = await registeredChildren();
|
|
116
|
+
creation(children, 'byFieldMetadata').should.deep.equal(creation(children, 'byKey'));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -20,8 +20,28 @@ export interface ChildrenFromMetadata {
|
|
|
20
20
|
readonly parentKey?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Options for the options form of the childrenFrom property decorator. */
|
|
24
|
+
export interface ChildrenFromOptions {
|
|
25
|
+
/** The child type, for TypeScript runtimes without emitted generic metadata. */
|
|
26
|
+
readonly childType?: Function;
|
|
27
|
+
/** The event property name used as the key for children. Defaults to the event source identifier. */
|
|
28
|
+
readonly key?: string;
|
|
29
|
+
/** The child model property name used to uniquely identify instances in the collection. */
|
|
30
|
+
readonly identifiedBy?: string;
|
|
31
|
+
/** The event property name used as the parent key. Defaults to the event source identifier. */
|
|
32
|
+
readonly parentKey?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const METADATA_KEY = 'chronicle:projection:childrenFrom';
|
|
24
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Property decorator that configures a children collection sub-projection from an event type.
|
|
39
|
+
* Use this form to name the child type and an explicit child key together.
|
|
40
|
+
* @param eventType - The event constructor.
|
|
41
|
+
* @param options - The child type, key, identifiedBy and parentKey.
|
|
42
|
+
* @returns A property decorator.
|
|
43
|
+
*/
|
|
44
|
+
export function childrenFrom(eventType: Function, options: ChildrenFromOptions): ChroniclePropertyDecorator;
|
|
25
45
|
/**
|
|
26
46
|
* Property decorator that configures a children collection sub-projection from an event type.
|
|
27
47
|
* @param eventType - The event constructor.
|
|
@@ -30,24 +50,40 @@ const METADATA_KEY = 'chronicle:projection:childrenFrom';
|
|
|
30
50
|
* @param parentKey - Optional event property name used as the parent key.
|
|
31
51
|
* @returns A property decorator.
|
|
32
52
|
*/
|
|
53
|
+
export function childrenFrom(eventType: Function, key?: string | Function, identifiedBy?: string, parentKey?: string): ChroniclePropertyDecorator;
|
|
33
54
|
export function childrenFrom(
|
|
34
55
|
eventType: Function,
|
|
35
|
-
|
|
56
|
+
keyOrOptions?: string | Function | ChildrenFromOptions,
|
|
36
57
|
identifiedBy?: string,
|
|
37
58
|
parentKey?: string
|
|
38
59
|
): ChroniclePropertyDecorator {
|
|
60
|
+
const metadata = toMetadata(eventType, keyOrOptions, identifiedBy, parentKey);
|
|
39
61
|
return decorateModelBoundProperty((target: object, propertyKey: string | symbol) => {
|
|
40
62
|
const propKey = propertyKey.toString();
|
|
41
63
|
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propKey);
|
|
42
64
|
const existing: ChildrenFromMetadata[] = Reflect.getMetadata(METADATA_KEY, target, propKey) ?? [];
|
|
43
|
-
const metadata: ChildrenFromMetadata = {
|
|
44
|
-
eventType, key: typeof key === 'string' ? key : undefined,
|
|
45
|
-
childType: typeof key === 'function' ? key : undefined, identifiedBy, parentKey
|
|
46
|
-
};
|
|
47
65
|
Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, propKey);
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
function toMetadata(
|
|
70
|
+
eventType: Function,
|
|
71
|
+
keyOrOptions: string | Function | ChildrenFromOptions | undefined,
|
|
72
|
+
identifiedBy: string | undefined,
|
|
73
|
+
parentKey: string | undefined
|
|
74
|
+
): ChildrenFromMetadata {
|
|
75
|
+
if (typeof keyOrOptions === 'object' && keyOrOptions !== null) {
|
|
76
|
+
return {
|
|
77
|
+
eventType, childType: keyOrOptions.childType, key: keyOrOptions.key,
|
|
78
|
+
identifiedBy: keyOrOptions.identifiedBy, parentKey: keyOrOptions.parentKey
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
eventType, key: typeof keyOrOptions === 'string' ? keyOrOptions : undefined,
|
|
83
|
+
childType: typeof keyOrOptions === 'function' ? keyOrOptions : undefined, identifiedBy, parentKey
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
/**
|
|
52
88
|
* Retrieves all childrenFrom metadata stored on the given property.
|
|
53
89
|
* @param target - The class prototype.
|
|
@@ -21,7 +21,7 @@ export type { DecrementMetadata } from './decrement.js';
|
|
|
21
21
|
export { count, getCountMetadata } from './count.js';
|
|
22
22
|
export type { CountMetadata } from './count.js';
|
|
23
23
|
export { childrenFrom, getChildrenFromMetadata } from './childrenFrom.js';
|
|
24
|
-
export type { ChildrenFromMetadata } from './childrenFrom.js';
|
|
24
|
+
export type { ChildrenFromMetadata, ChildrenFromOptions } from './childrenFrom.js';
|
|
25
25
|
export { nested, isNested } from './nested.js';
|
|
26
26
|
export { clearWith, getClearWithClassMetadata, getClearWithPropertyMetadata } from './clearWith.js';
|
|
27
27
|
export type { ClearWithMetadata } from './clearWith.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
6
|
+
import { childrenFrom } from '../projections/modelBound/childrenFrom.js';
|
|
7
|
+
import { fromEvent } from '../projections/modelBound/fromEvent.js';
|
|
8
|
+
import { ReadModelScenario, UnsupportedProjectionOperation } from './index.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
class ScenarioItemAdded {
|
|
13
|
+
itemId = '';
|
|
14
|
+
name = '';
|
|
15
|
+
}
|
|
16
|
+
eventType('scenario-keyed-item-added')(ScenarioItemAdded);
|
|
17
|
+
|
|
18
|
+
class ScenarioItem {
|
|
19
|
+
id = '';
|
|
20
|
+
name = '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ScenarioOrder {
|
|
24
|
+
items: ScenarioItem[] = [];
|
|
25
|
+
}
|
|
26
|
+
childrenFrom(ScenarioItemAdded, { childType: ScenarioItem, key: 'itemId' })(ScenarioOrder.prototype, 'items');
|
|
27
|
+
fromEvent(ScenarioItemAdded)(ScenarioOrder);
|
|
28
|
+
|
|
29
|
+
describe('when a read model scenario evaluates keyed children from the options form', () => {
|
|
30
|
+
it('should reject children projections explicitly when the scenario is created', () => {
|
|
31
|
+
(() => new ReadModelScenario(ScenarioOrder, { eventTypes: [ScenarioItemAdded], reducers: [], projections: [] }))
|
|
32
|
+
.should.throw(UnsupportedProjectionOperation)
|
|
33
|
+
.with.property('message').that.includes('Children.items (@childrenFrom)').and.includes('children projections');
|
|
34
|
+
});
|
|
35
|
+
});
|