Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 31 additions & 25 deletions Documentation/screenplay/projections/auto-map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,39 @@ projection User => UserReadModel

All properties must be mapped explicitly.

### Event-Level Disable
### Every- and All-Level Disable

Disable AutoMap for a specific event:
Disable AutoMap inside an `every` or `all` block:

```pdl
projection User => UserReadModel
from UserRegistered
every
no automap
Name = name
Email = email

from UserUpdated
# AutoMap still enabled here
UpdatedAt = $eventContext.occurred
LastUpdated = $eventContext.occurred
```

AutoMap cannot be toggled inside an individual `from` block — it is controlled at the projection, children, or nested level.

### Join-Level Disable

Disable AutoMap within join blocks:
Disable AutoMap within a join's `with` blocks:

```pdl
join Group on GroupId
events GroupCreated, GroupRenamed
no automap
Name = name
with GroupCreated
no automap
Name = name
with GroupRenamed
no automap
Name = name
```

### Children-Level Disable

Disable AutoMap for children collections:

```pdl
children members id userId
children members identified by userId
no automap

from UserAddedToGroup
Expand Down Expand Up @@ -107,12 +107,14 @@ With default AutoMap, `Name`, `Email`, and `Age` are automatically copied. `IsAc
## Scope and Precedence

1. **Projection-level** `no automap` disables AutoMap for all events
2. **Event-level** `no automap` applies only to that specific event
2. **Every/All-level** `no automap` applies within that `every` or `all` block
3. **Explicit mappings** always override AutoMap values when both exist
4. **Join-level** `no automap` applies to joined events
4. **Join-level** `no automap` applies within the individual `with` block
5. **Children-level** `no automap` applies to child events
6. **Default is Enabled** - AutoMap works unless explicitly disabled

AutoMap cannot be toggled inside an individual `from` block.

## Examples

### Default AutoMap
Expand Down Expand Up @@ -145,11 +147,12 @@ projection User => UserReadModel
# AutoMap enabled (default)
IsActive = true

from UserProfileUpdated
no automap
# Must map everything explicitly
Name = fullName
Email = emailAddress
join Profile on ProfileId
with UserProfileUpdated
no automap
# Must map everything explicitly
Name = fullName
Email = emailAddress
```

### Projection-Level Disable with Event-Level Override
Expand All @@ -176,9 +179,12 @@ projection Order => OrderReadModel
# AutoMap enabled

join Customer on CustomerId
events CustomerCreated, CustomerUpdated
no automap
CustomerName = name
with CustomerCreated
no automap
CustomerName = name
with CustomerUpdated
no automap
CustomerName = name
```

### Children with Selective Disable
Expand All @@ -188,7 +194,7 @@ projection Group => GroupReadModel
from GroupCreated
# AutoMap enabled

children members id userId
children members identified by userId
no automap

from UserAddedToGroup
Expand Down
13 changes: 9 additions & 4 deletions Documentation/screenplay/projections/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Projection = "projection", Ident, [ "=>", TypeRef ], NL,
DEDENT ] ;

ProjDirective = "no", "automap", NL
| "sequence", Ident, NL
| KeyDecl
| CompositeKeyDecl ;

Expand Down Expand Up @@ -78,6 +79,7 @@ JoinBlock = "join", Ident, "on", Ident, NL,

WithEventBlock = "with", TypeRef, NL,
[ INDENT,
[ "automap" | "no", "automap", NL ],
{ MappingLine },
DEDENT ] ;

Expand Down Expand Up @@ -275,11 +277,12 @@ JoinBlock = "join", Ident, "on", Ident, NL,

WithEventBlock = "with", TypeRef, NL,
[ INDENT,
[ "automap" | "no", "automap", NL ],
{ MappingLine },
DEDENT ] ;
```

**Note:** AutoMap for join blocks is controlled at the projection or children level, not within individual with blocks.
**Note:** Each `with` block may toggle AutoMap for its own mappings with `automap` or `no automap`.

### Children Block

Expand Down Expand Up @@ -461,8 +464,10 @@ projection Order => OrderReadModel
ShippedAt = $eventContext.occurred

join Customer on CustomerId
events CustomerCreated, CustomerUpdated
CustomerName = name
with CustomerCreated
CustomerName = name
with CustomerUpdated
CustomerName = name

children items identified by lineNumber
every
Expand All @@ -484,7 +489,7 @@ This projection uses:
- Projection declaration
- Every block with exclude children at the projection level
- Multiple from blocks with keys
- Join block with multiple events
- Join block with multiple with blocks
- Children block with:
- Child every block for common mappings across all child events
- Nested from and remove blocks
Expand Down
26 changes: 19 additions & 7 deletions Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
domain Sales

import Customers.CustomerRegistered
import Customers.CustomerAccountClosed
import Shipping.InvoiceShipped
import Shipping.InvoiceShippingCleared

concept InvoiceId : Uuid
concept CustomerId : Uuid
Expand Down Expand Up @@ -554,8 +557,7 @@ module Invoicing
authorize IsAuthenticated

projection InvoiceList => InvoiceListReadModel
sequence invoices
all
every
lastActivityAt = $eventContext.occurred
from InvoiceRegistered key invoiceId
id = $eventSourceId
Expand All @@ -565,7 +567,7 @@ module Invoicing
status = "draft"
lineCount = 0
dueDate = dueDate
channel = literal "portal"
channel = "portal"
from InvoiceSent, InvoicePaid
touched = true
from InvoiceMarkedOverdue
Expand Down Expand Up @@ -609,7 +611,7 @@ module Invoicing
registeredForSubject = $causedBy.subject
displayLabel = `${invoiceNumber} (${currency})`
from InvoiceSent
set status to "sent"
status = "sent"
sentAt = sentAt
from InvoicePaid
status = "paid"
Expand Down Expand Up @@ -665,7 +667,6 @@ module Invoicing
authorize IsAccountant

projection InvoiceLineReport => InvoiceLineReportReadModel
automap
from InvoiceLineItemAdded
key InvoiceLineKey
invoiceId = invoiceId
Expand Down Expand Up @@ -696,11 +697,11 @@ module Invoicing
from InvoicePaid
decrement sentCount
increment paidCount
add paidTotal by amount
from InvoiceMarkedOverdue
increment overdueCount
from InvoiceWrittenOff
subtract paidTotal by amount
add writeOffTotal by amount
subtract outstandingTotal by amount
from InvoiceCancelled
decrement totalCount

Expand Down Expand Up @@ -754,6 +755,17 @@ module Invoicing
label $strings.invoices.actions.newInvoice
navigate to RegisterInvoiceScreen

slice StateView SystemActivity
description "Counts every event in the system for the activity ticker"

query GetSystemActivity => SystemActivityReadModel
authorize IsAccountant

projection SystemActivity => SystemActivityReadModel
all
count totalEvents
lastOccurred = $eventContext.occurred

slice Automation NotifyCustomerOnInvoiceRegistered

reactor NotifyCustomer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ void Because()
[Fact] void should_succeed() => _result.Success.ShouldBeTrue();
[Fact] void should_have_no_diagnostics() => _result.Diagnostics.ShouldBeEmpty();
[Fact] void should_have_the_domain() => _result.Value!.Domain!.Name.ShouldEqual("Sales");
[Fact] void should_have_the_import() => _result.Value!.Imports.Single().Name.ShouldEqual("CustomerRegistered");
[Fact] void should_have_all_imports() => _result.Value!.Imports.Count().ShouldEqual(4);
[Fact] void should_have_the_customer_import() => _result.Value!.Imports.Select(_ => _.Name).ShouldContain("CustomerRegistered");
[Fact] void should_have_all_concepts() => _result.Value!.Concepts.Count().ShouldEqual(12);
[Fact] void should_have_the_enum_concept_values() => _result.Value!.Concepts.Single(_ => _.Name == "InvoiceStatus").Values.Count().ShouldEqual(5);
[Fact] void should_capture_pii_attributes() => _result.Value!.Concepts.Single(_ => _.Name == "EmailAddress").Attributes.ShouldContain("pii");
Expand All @@ -44,7 +45,7 @@ void Because()
[Fact] void should_have_the_command_description() => RegisterCommand.Description.ShouldEqual("Registers a new invoice with its lines and payment terms");
[Fact] void should_have_both_layouts() => _result.Value!.Modules.Single().Layouts.Count().ShouldEqual(2);
[Fact] void should_have_the_master_detail_slots() => _result.Value!.Modules.Single().Layouts.First().Slots.ShouldContainOnly("sidebar", "main");
[Fact] void should_have_all_slices() => _feature.Slices.Count().ShouldEqual(12);
[Fact] void should_have_all_slices() => _feature.Slices.Count().ShouldEqual(13);
[Fact] void should_have_the_nested_feature() => _feature.Features.Single().Name.ShouldEqual("Adjustments");
[Fact] void should_have_the_nested_feature_slices() => _feature.Features.Single().Slices.Select(_ => _.Name).ShouldContainOnly("ApplyDiscount", "WriteOffInvoice");
[Fact] void should_parse_conditional_produces() => RegisterCommand.Produces.Count(_ => _.When is not null).ShouldEqual(4);
Expand Down Expand Up @@ -88,7 +89,6 @@ void Because()
[Fact] void should_parse_the_details_projection_every() => Slice("InvoiceDetails").Projection!.Blocks.OfType<EverySyntax>().Single().IncludeChildren.ShouldBeFalse();
[Fact] void should_parse_the_details_projection_remove_via_join() => Slice("InvoiceDetails").Projection!.Blocks.OfType<RemoveViaJoinSyntax>().Single().Event.ShouldEqual("CustomerAccountClosed");
[Fact] void should_parse_the_details_projection_nested() => Slice("InvoiceDetails").Projection!.Blocks.OfType<NestedSyntax>().Single().Property.ShouldEqual("shipping");
[Fact] void should_parse_the_list_projection_sequence() => Slice("InvoiceList").Projection!.Sequence.ShouldEqual("invoices");
[Fact] void should_parse_the_line_report_composite_key() => ((CompositeKeySyntax)Slice("InvoiceLineReport").Projection!.Blocks.OfType<FromSyntax>().Single().Key!).Type.ShouldEqual("InvoiceLineKey");
[Fact] void should_parse_the_summary_counters() => Slice("InvoiceDashboard").Projection!.Blocks.OfType<FromSyntax>().First().Mappings.OfType<IncrementMappingSyntax>().Count().ShouldEqual(2);
[Fact] void should_parse_the_dashboard_screen_layout() => Slice("InvoiceDashboard").Screens.Single().Directives.OfType<ScreenLayoutSyntax>().Single().Slots.Count().ShouldEqual(4);
Expand Down
26 changes: 19 additions & 7 deletions Source/Screenplay/Monaco/screenplay-editor/samples/invoicing.play
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
domain Sales

import Customers.CustomerRegistered
import Customers.CustomerAccountClosed
import Shipping.InvoiceShipped
import Shipping.InvoiceShippingCleared

concept InvoiceId : Uuid
concept CustomerId : Uuid
Expand Down Expand Up @@ -554,8 +557,7 @@ module Invoicing
authorize IsAuthenticated

projection InvoiceList => InvoiceListReadModel
sequence invoices
all
every
lastActivityAt = $eventContext.occurred
from InvoiceRegistered key invoiceId
id = $eventSourceId
Expand All @@ -565,7 +567,7 @@ module Invoicing
status = "draft"
lineCount = 0
dueDate = dueDate
channel = literal "portal"
channel = "portal"
from InvoiceSent, InvoicePaid
touched = true
from InvoiceMarkedOverdue
Expand Down Expand Up @@ -609,7 +611,7 @@ module Invoicing
registeredForSubject = $causedBy.subject
displayLabel = `${invoiceNumber} (${currency})`
from InvoiceSent
set status to "sent"
status = "sent"
sentAt = sentAt
from InvoicePaid
status = "paid"
Expand Down Expand Up @@ -665,7 +667,6 @@ module Invoicing
authorize IsAccountant

projection InvoiceLineReport => InvoiceLineReportReadModel
automap
from InvoiceLineItemAdded
key InvoiceLineKey
invoiceId = invoiceId
Expand Down Expand Up @@ -696,11 +697,11 @@ module Invoicing
from InvoicePaid
decrement sentCount
increment paidCount
add paidTotal by amount
from InvoiceMarkedOverdue
increment overdueCount
from InvoiceWrittenOff
subtract paidTotal by amount
add writeOffTotal by amount
subtract outstandingTotal by amount
from InvoiceCancelled
decrement totalCount

Expand Down Expand Up @@ -754,6 +755,17 @@ module Invoicing
label $strings.invoices.actions.newInvoice
navigate to RegisterInvoiceScreen

slice StateView SystemActivity
description "Counts every event in the system for the activity ticker"

query GetSystemActivity => SystemActivityReadModel
authorize IsAccountant

projection SystemActivity => SystemActivityReadModel
all
count totalEvents
lastOccurred = $eventContext.occurred

slice Automation NotifyCustomerOnInvoiceRegistered

reactor NotifyCustomer
Expand Down
Loading