// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { field } from '@cratis/fundamentals'; import { chai, describe, it, type Assertion } from 'vitest'; import { fromAllEvents, getFromAllEventsMetadata, fromAll, getFromAllMetadata, fromEvery, getFromEveryMetadata, childrenFrom, nested } from '../index.js'; import { eventType } from '../../events/eventTypeDecorator.js'; import { TypeIntrospector } from '../../types/index.js'; import { ReadModelScenario } from '../../testing/ReadModelScenario.js'; import { UnsupportedProjectionOperation } from '../../testing/projections/UnsupportedProjectionOperation.js'; import { compileModelBound } from './given/compile_model_bound.js'; chai.should(); function should(value: unknown): Assertion { return (value as { should: Assertion }).should; } class AllModel { @field(String) id = ''; @field(String) @fromAllEvents('name') title = ''; @field(String) @fromAllEvents(undefined, 'sequenceNumber') sequence = ''; @field(String) @fromEvery(undefined, 'eventSourceId') source = ''; constructor() { throw new Error('Model discovery must not construct the model'); } } describe('when compiling standard fromAllEvents without explicit event subscriptions', () => { it('should emit the subscription and shared mappings without constructing the model', () => { const { definition } = compileModelBound(AllModel); should(definition.SubscribesToAllEvents).be.true; should(definition.From).deep.equal([]); should(definition.FromEvery).deep.equal([]); should(definition.All).deep.equal({ Properties: { title: 'name', sequence: '$eventContext(SequenceNumber)', source: '$eventContext(EventSourceId)' }, IncludeChildren: true, AutoMap: 0 }); }); it('should discover a model using only fromAllEvents through existing Arc style readers', () => { class OnlyAll { @field(String) @fromAllEvents() title = ''; constructor() { throw new Error('Discovery must not construct the model'); } } class Derived extends OnlyAll {} for (const type of [OnlyAll, Derived]) { should(TypeIntrospector.getTrackedProperties(type).some(property => !!getFromEveryMetadata(type.prototype, property) || !!getFromAllMetadata(type.prototype, property))).be.true; should(getFromAllMetadata(type.prototype, 'title')).deep.equal({ property: undefined, contextProperty: undefined }); should(getFromEveryMetadata(type.prototype, 'title') === undefined).be.true; should(compileModelBound(type).definition.SubscribesToAllEvents).be.true; } }); it('should inherit and override mappings without leaking the subscription to a sibling', () => { class Base { @field(String) id = ''; } class All extends Base { @field(String) @fromAllEvents('name') title = ''; } class Derived extends All {} class Override extends All { @field(String) @fromAllEvents('derivedName') override title = ''; } class Alias extends Base { @field(String) @fromAll('name') title = ''; } should(compileModelBound(Derived).definition.SubscribesToAllEvents).be.true; should(compileModelBound(Derived).definition.All).deep.equal(compileModelBound(All).definition.All); should(compileModelBound(Override).definition.All!.Properties.title).equal('derivedName'); should(compileModelBound(All).definition.All!.Properties.title).equal('name'); should(compileModelBound(Alias).definition.SubscribesToAllEvents === undefined).be.true; should(getFromAllEventsMetadata(Alias.prototype, 'title') === undefined).be.true; }); it('should collect standard child and nested mappings on their owning levels without widening the subscription', () => { @eventType('standard-from-all-events-child-added') class Added { @field(String) id = ''; } class Detail { @field(String) @fromAllEvents(undefined, 'sequenceNumber') sequence = ''; @field(String) @fromAllEvents() label = ''; } class Child { @field(String) id = ''; @field(String) @fromAllEvents('name') title = ''; @field(String) @fromEvery('everyName') @fromAll('aliasName') restricted = ''; @field(Detail) @nested detail = new Detail(); } class Derived extends Child {} class Sibling extends Child { @field(String) @fromAllEvents('siblingName') override title = ''; } class Group { @field(Array, { genericArguments: [Derived] }) @childrenFrom(Added, 'id') items: Derived[] = []; } class Parent { @field(Array, { genericArguments: [Group] }) @childrenFrom(Added, 'id') groups: Group[] = []; @field(Array, { genericArguments: [Sibling] }) @childrenFrom(Added, 'id') siblings: Sibling[] = []; @field(Detail) @nested detail = new Detail(); } const { compiled, definition } = compileModelBound(Parent); should(definition.All).deep.equal({ Properties: {}, IncludeChildren: false, AutoMap: 0 }); should(definition.SubscribesToAllEvents === undefined).be.true; should(definition.Children.groups.All!.Properties).deep.equal({}); should(definition.Children.groups.Children.items.All!.Properties).deep.equal({ title: 'name', restricted: 'everyName' }); should(definition.Children.siblings.All!.Properties).deep.equal({ title: 'siblingName', restricted: 'everyName' }); const detailMappings = { sequence: '$eventContext(SequenceNumber)', label: 'label' }; should(definition.Children.groups.Children.items.Nested.detail.All!.Properties).deep.equal(detailMappings); should(definition.Nested.detail.All!.Properties).deep.equal(detailMappings); should(compiled.provenance.get(definition)).include.deep.members([ { contractPath: 'Children.groups.Children.items.All.Properties.restricted', declaration: '@fromEvery' }, { contractPath: 'Children.groups.Children.items.Nested.detail.All.Properties.sequence', declaration: '@fromAllEvents' } ]); }); it('should preserve independent same-named root and child mappings in either property order', () => { @eventType('standard-all-same-name-added') class Added { @field(String) id = ''; } class Child { @field(String) id = ''; @field(String) @fromEvery('ignored') @fromAllEvents('childName') title = ''; @field(String) @fromAllEvents('childOther') @fromEvery('ignored') other = ''; } class RootFirst { @field(String) @fromAllEvents('rootName') title = ''; @field(Array, { genericArguments: [Child] }) @childrenFrom(Added, 'id') items: Child[] = []; } class ChildFirst { @field(Array, { genericArguments: [Child] }) @childrenFrom(Added, 'id') items: Child[] = []; @field(String) @fromAllEvents('rootName') title = ''; } for (const type of [RootFirst, ChildFirst]) { const { definition } = compileModelBound(type); should(definition.All!.Properties).deep.equal({ title: 'rootName' }); should(definition.Children.items.All!.Properties).deep.equal({ title: 'childName', other: 'childOther' }); should(definition.SubscribesToAllEvents).be.true; } }); it('should reject all-event scenarios before replay with the standard decorator declaration', () => { should(() => new ReadModelScenario(AllModel, { readModels: [AllModel], eventTypes: [], projections: [], reducers: [] })) .throw(UnsupportedProjectionOperation, 'SubscribesToAllEvents (@fromAllEvents)'); }); it('should prefer fromAllEvents in either decorator order without changing the alias metadata', () => { class Both { @field(String) id = ''; @field(String) @fromEvery('everyName') @fromAll('aliasName') @fromAllEvents('newName') title = ''; @field(String) @fromAllEvents('otherName') @fromAll('aliasName') @fromEvery('everyName') other = ''; } const { compiled, definition } = compileModelBound(Both); should(definition.SubscribesToAllEvents).be.true; should(definition.All!.Properties).deep.equal({ title: 'newName', other: 'otherName' }); should(getFromAllMetadata(Both.prototype, 'title')!.property).equal('aliasName'); should(getFromEveryMetadata(Both.prototype, 'title')!.property).equal('everyName'); should(compiled.provenance.get(definition)).include.deep.members([ { contractPath: 'SubscribesToAllEvents', declaration: '@fromAllEvents' }, { contractPath: 'All.Properties.title', declaration: '@fromAllEvents' } ]); }); });