@auto-engineer/server-generator-apollo-emmett 1.130.0 → 1.134.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +168 -0
- package/dist/src/codegen/extract/data-sink.d.ts +1 -0
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.js +9 -0
- package/dist/src/codegen/extract/data-sink.js.map +1 -1
- package/dist/src/codegen/extract/imports.d.ts +0 -5
- package/dist/src/codegen/extract/imports.d.ts.map +1 -1
- package/dist/src/codegen/extract/imports.js +0 -7
- package/dist/src/codegen/extract/imports.js.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +6 -1
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.js +17 -0
- package/dist/src/codegen/extract/projection.js.map +1 -1
- package/dist/src/codegen/extract/query.d.ts +8 -2
- package/dist/src/codegen/extract/query.d.ts.map +1 -1
- package/dist/src/codegen/extract/query.js +36 -0
- package/dist/src/codegen/extract/query.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +13 -5
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/query/events.specs.ts +112 -0
- package/dist/src/codegen/templates/query/events.ts.ejs +17 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +323 -0
- package/dist/src/codegen/templates/query/projection.ts.ejs +27 -4
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +242 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
- package/dist/src/commands/generate-server.js +5 -5
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/data-sink.specs.ts +24 -0
- package/src/codegen/extract/data-sink.ts +10 -0
- package/src/codegen/extract/imports.ts +0 -8
- package/src/codegen/extract/projection.specs.ts +212 -0
- package/src/codegen/extract/projection.ts +22 -1
- package/src/codegen/extract/query.specs.ts +137 -0
- package/src/codegen/extract/query.ts +46 -2
- package/src/codegen/scaffoldFromSchema.ts +18 -3
- package/src/codegen/templates/query/events.specs.ts +112 -0
- package/src/codegen/templates/query/events.ts.ejs +17 -0
- package/src/codegen/templates/query/projection.specs.ts +323 -0
- package/src/codegen/templates/query/projection.ts.ejs +27 -4
- package/src/codegen/templates/query/query.resolver.specs.ts +242 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
- package/src/commands/generate-server.ts +5 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Model } from '@auto-engineer/narrative';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
4
|
+
|
|
5
|
+
describe('query events.ts.ejs', () => {
|
|
6
|
+
it('generates events.ts for orphan events not produced by any command slice', async () => {
|
|
7
|
+
const spec: Model = {
|
|
8
|
+
variant: 'specs',
|
|
9
|
+
narratives: [
|
|
10
|
+
{
|
|
11
|
+
name: 'appointment-flow',
|
|
12
|
+
slices: [
|
|
13
|
+
{
|
|
14
|
+
type: 'query',
|
|
15
|
+
name: 'view-appointments',
|
|
16
|
+
stream: 'appointments',
|
|
17
|
+
client: { specs: [] },
|
|
18
|
+
server: {
|
|
19
|
+
description: 'projection for booked appointments',
|
|
20
|
+
data: {
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
target: { type: 'State', name: 'Appointment' },
|
|
24
|
+
origin: {
|
|
25
|
+
type: 'projection',
|
|
26
|
+
name: 'AppointmentsProjection',
|
|
27
|
+
idField: 'appointmentId',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
specs: [
|
|
33
|
+
{
|
|
34
|
+
type: 'gherkin',
|
|
35
|
+
feature: 'View appointments query',
|
|
36
|
+
rules: [
|
|
37
|
+
{
|
|
38
|
+
name: 'Should project appointments',
|
|
39
|
+
examples: [
|
|
40
|
+
{
|
|
41
|
+
name: 'Appointment booked shows in list',
|
|
42
|
+
steps: [
|
|
43
|
+
{
|
|
44
|
+
keyword: 'When',
|
|
45
|
+
text: 'AppointmentBooked',
|
|
46
|
+
docString: {
|
|
47
|
+
appointmentId: 'appt_1',
|
|
48
|
+
date: '2024-06-15',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
keyword: 'Then',
|
|
53
|
+
text: 'Appointment',
|
|
54
|
+
docString: {
|
|
55
|
+
appointmentId: 'appt_1',
|
|
56
|
+
date: '2024-06-15',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
type: 'event',
|
|
74
|
+
name: 'AppointmentBooked',
|
|
75
|
+
source: 'internal',
|
|
76
|
+
fields: [
|
|
77
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
78
|
+
{ name: 'date', type: 'string', required: true },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'state',
|
|
83
|
+
name: 'Appointment',
|
|
84
|
+
fields: [
|
|
85
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
86
|
+
{ name: 'date', type: 'string', required: true },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
93
|
+
|
|
94
|
+
const eventFile = plans.find((p) => p.outputPath.endsWith('view-appointments/events.ts'));
|
|
95
|
+
expect(eventFile).toBeDefined();
|
|
96
|
+
expect(eventFile?.contents).toMatchInlineSnapshot(`
|
|
97
|
+
"import type { Event } from '@event-driven-io/emmett';
|
|
98
|
+
|
|
99
|
+
export type AppointmentBooked = Event<
|
|
100
|
+
'AppointmentBooked',
|
|
101
|
+
{
|
|
102
|
+
appointmentId: string;
|
|
103
|
+
date: string;
|
|
104
|
+
}
|
|
105
|
+
>;
|
|
106
|
+
"
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
const projectionFile = plans.find((p) => p.outputPath.endsWith('view-appointments/projection.ts'));
|
|
110
|
+
expect(projectionFile?.contents).toContain("import type { AppointmentBooked } from './events'");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const enumList = collectEnumNames(localEvents.flatMap(e => e.fields));
|
|
3
|
+
%><% if (localEvents.length) { -%>
|
|
4
|
+
import type { Event } from '@event-driven-io/emmett';
|
|
5
|
+
<% if (enumList.length > 0) { %>import { <%= enumList.join(', ') %> } from '../../../shared';
|
|
6
|
+
<% } %>
|
|
7
|
+
<% for (const event of localEvents) { -%>
|
|
8
|
+
export type <%= pascalCase(event.type) %> = Event<
|
|
9
|
+
'<%= event.type %>',
|
|
10
|
+
{
|
|
11
|
+
<% for (const field of event.fields) { -%>
|
|
12
|
+
<%- field.name %>: <%- toTsFieldType(field.tsType) %>;
|
|
13
|
+
<% } -%>
|
|
14
|
+
}
|
|
15
|
+
>;
|
|
16
|
+
<% } -%>
|
|
17
|
+
<% } -%>
|
|
@@ -1008,4 +1008,327 @@ describe('projection.ts.ejs', () => {
|
|
|
1008
1008
|
expect(projectionFile?.contents).not.toContain('number | "pending"');
|
|
1009
1009
|
expect(projectionFile?.contents).toContain('mixedStatus: /* TODO: map from event.data */ undefined as any');
|
|
1010
1010
|
});
|
|
1011
|
+
|
|
1012
|
+
it('should generate switch-based getDocumentId when events come from slices with different id fields', async () => {
|
|
1013
|
+
const flows: Model = {
|
|
1014
|
+
variant: 'specs',
|
|
1015
|
+
narratives: [
|
|
1016
|
+
{
|
|
1017
|
+
name: 'workout-flow',
|
|
1018
|
+
slices: [
|
|
1019
|
+
{
|
|
1020
|
+
type: 'command',
|
|
1021
|
+
name: 'create-workout',
|
|
1022
|
+
stream: 'workouts-${id}',
|
|
1023
|
+
client: { specs: [] },
|
|
1024
|
+
server: {
|
|
1025
|
+
description: 'creates a workout',
|
|
1026
|
+
specs: [
|
|
1027
|
+
{
|
|
1028
|
+
type: 'gherkin',
|
|
1029
|
+
feature: 'Create workout',
|
|
1030
|
+
rules: [
|
|
1031
|
+
{
|
|
1032
|
+
name: 'rule',
|
|
1033
|
+
examples: [
|
|
1034
|
+
{
|
|
1035
|
+
name: 'success',
|
|
1036
|
+
steps: [
|
|
1037
|
+
{ keyword: 'When', text: 'CreateWorkout', docString: { id: 'w1', name: 'Leg Day' } },
|
|
1038
|
+
{ keyword: 'Then', text: 'WorkoutCreated', docString: { id: 'w1', name: 'Leg Day' } },
|
|
1039
|
+
],
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
type: 'command',
|
|
1050
|
+
name: 'log-exercises',
|
|
1051
|
+
stream: 'workouts-${workoutId}',
|
|
1052
|
+
client: { specs: [] },
|
|
1053
|
+
server: {
|
|
1054
|
+
description: 'logs exercises to a workout',
|
|
1055
|
+
specs: [
|
|
1056
|
+
{
|
|
1057
|
+
type: 'gherkin',
|
|
1058
|
+
feature: 'Log exercises',
|
|
1059
|
+
rules: [
|
|
1060
|
+
{
|
|
1061
|
+
name: 'rule',
|
|
1062
|
+
examples: [
|
|
1063
|
+
{
|
|
1064
|
+
name: 'success',
|
|
1065
|
+
steps: [
|
|
1066
|
+
{
|
|
1067
|
+
keyword: 'When',
|
|
1068
|
+
text: 'LogExercises',
|
|
1069
|
+
docString: { workoutId: 'w1', exercise: 'Squats' },
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
keyword: 'Then',
|
|
1073
|
+
text: 'ExerciseLogged',
|
|
1074
|
+
docString: { workoutId: 'w1', exercise: 'Squats' },
|
|
1075
|
+
},
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
type: 'query',
|
|
1087
|
+
name: 'view-workout',
|
|
1088
|
+
stream: 'workouts',
|
|
1089
|
+
client: { specs: [] },
|
|
1090
|
+
server: {
|
|
1091
|
+
description: 'projection for workout details',
|
|
1092
|
+
data: {
|
|
1093
|
+
items: [
|
|
1094
|
+
{
|
|
1095
|
+
target: { type: 'State', name: 'WorkoutView' },
|
|
1096
|
+
origin: {
|
|
1097
|
+
type: 'projection',
|
|
1098
|
+
name: 'WorkoutProjection',
|
|
1099
|
+
idField: 'id',
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1104
|
+
specs: [
|
|
1105
|
+
{
|
|
1106
|
+
type: 'gherkin',
|
|
1107
|
+
feature: 'View workout',
|
|
1108
|
+
rules: [
|
|
1109
|
+
{
|
|
1110
|
+
name: 'rule',
|
|
1111
|
+
examples: [
|
|
1112
|
+
{
|
|
1113
|
+
name: 'workout created',
|
|
1114
|
+
steps: [
|
|
1115
|
+
{
|
|
1116
|
+
keyword: 'When',
|
|
1117
|
+
text: 'WorkoutCreated',
|
|
1118
|
+
docString: { id: 'w1', name: 'Leg Day' },
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
keyword: 'Then',
|
|
1122
|
+
text: 'WorkoutView',
|
|
1123
|
+
docString: { id: 'w1', name: 'Leg Day', exercises: [] },
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
name: 'exercise logged',
|
|
1129
|
+
steps: [
|
|
1130
|
+
{
|
|
1131
|
+
keyword: 'When',
|
|
1132
|
+
text: 'ExerciseLogged',
|
|
1133
|
+
docString: { workoutId: 'w1', exercise: 'Squats' },
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
keyword: 'Then',
|
|
1137
|
+
text: 'WorkoutView',
|
|
1138
|
+
docString: { id: 'w1', name: 'Leg Day', exercises: ['Squats'] },
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
},
|
|
1151
|
+
],
|
|
1152
|
+
messages: [
|
|
1153
|
+
{
|
|
1154
|
+
type: 'command',
|
|
1155
|
+
name: 'CreateWorkout',
|
|
1156
|
+
fields: [
|
|
1157
|
+
{ name: 'id', type: 'string', required: true },
|
|
1158
|
+
{ name: 'name', type: 'string', required: true },
|
|
1159
|
+
],
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
type: 'command',
|
|
1163
|
+
name: 'LogExercises',
|
|
1164
|
+
fields: [
|
|
1165
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
1166
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
1167
|
+
],
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
type: 'event',
|
|
1171
|
+
name: 'WorkoutCreated',
|
|
1172
|
+
source: 'internal',
|
|
1173
|
+
fields: [
|
|
1174
|
+
{ name: 'id', type: 'string', required: true },
|
|
1175
|
+
{ name: 'name', type: 'string', required: true },
|
|
1176
|
+
],
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
type: 'event',
|
|
1180
|
+
name: 'ExerciseLogged',
|
|
1181
|
+
source: 'internal',
|
|
1182
|
+
fields: [
|
|
1183
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
1184
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
1185
|
+
],
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
type: 'state',
|
|
1189
|
+
name: 'WorkoutView',
|
|
1190
|
+
fields: [
|
|
1191
|
+
{ name: 'id', type: 'string', required: true },
|
|
1192
|
+
{ name: 'name', type: 'string', required: true },
|
|
1193
|
+
{ name: 'exercises', type: 'Array<string>', required: true },
|
|
1194
|
+
],
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const { plans } = await generateScaffoldFilePlans(flows.narratives, flows.messages, undefined, 'src/domain/flows');
|
|
1200
|
+
const projectionFile = plans.find((p) => p.outputPath.endsWith('view-workout/projection.ts'));
|
|
1201
|
+
|
|
1202
|
+
expect(projectionFile?.contents).toMatchInlineSnapshot(`
|
|
1203
|
+
"import {
|
|
1204
|
+
inMemorySingleStreamProjection,
|
|
1205
|
+
type ReadEvent,
|
|
1206
|
+
type InMemoryReadEventMetadata,
|
|
1207
|
+
} from '@event-driven-io/emmett';
|
|
1208
|
+
import type { WorkoutView } from './state';
|
|
1209
|
+
import type { WorkoutCreated } from '../create-workout/events';
|
|
1210
|
+
|
|
1211
|
+
import type { ExerciseLogged } from '../log-exercises/events';
|
|
1212
|
+
|
|
1213
|
+
type AllEvents = ExerciseLogged | WorkoutCreated;
|
|
1214
|
+
|
|
1215
|
+
// Auto-generated — do not change this function, its imports, or type parameters.
|
|
1216
|
+
// Only implement the case bodies inside evolve.
|
|
1217
|
+
export const projection = inMemorySingleStreamProjection<WorkoutView, AllEvents>({
|
|
1218
|
+
collectionName: 'WorkoutProjection',
|
|
1219
|
+
canHandle: ['WorkoutCreated', 'ExerciseLogged'],
|
|
1220
|
+
getDocumentId: (event) => {
|
|
1221
|
+
switch (event.type) {
|
|
1222
|
+
case 'WorkoutCreated':
|
|
1223
|
+
return event.data.id;
|
|
1224
|
+
|
|
1225
|
+
case 'ExerciseLogged':
|
|
1226
|
+
return event.data.workoutId;
|
|
1227
|
+
|
|
1228
|
+
default:
|
|
1229
|
+
return event.data.id;
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
evolve: (
|
|
1233
|
+
document: WorkoutView | null,
|
|
1234
|
+
event: ReadEvent<AllEvents, InMemoryReadEventMetadata>,
|
|
1235
|
+
): WorkoutView | null => {
|
|
1236
|
+
switch (event.type) {
|
|
1237
|
+
case 'WorkoutCreated': {
|
|
1238
|
+
/**
|
|
1239
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
1240
|
+
*
|
|
1241
|
+
* Derive ALL field values from event.data or existing document state.
|
|
1242
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
1243
|
+
* Preserve all import paths above — they are generated from the model.
|
|
1244
|
+
* ⚠️ \`document\` may be null (first event for this entity). Guard before accessing properties.
|
|
1245
|
+
*
|
|
1246
|
+
* CONSTRAINTS:
|
|
1247
|
+
* - NEVER use \`as SomeType\` type assertions. Declare typed variables instead: \`const x: Type = value;\`
|
|
1248
|
+
* - Only reference event.data fields listed in the "Event fields:" line below.
|
|
1249
|
+
* - Do NOT modify anything outside this case block: imports, type parameters, canHandle, collectionName, and getDocumentId are auto-generated.
|
|
1250
|
+
*
|
|
1251
|
+
* Implement how this event updates the projection.
|
|
1252
|
+
*
|
|
1253
|
+
* **Internal State Pattern (extends, never replaces the imported type):**
|
|
1254
|
+
* If you need to track state beyond the public WorkoutView type (e.g., to calculate
|
|
1255
|
+
* aggregations, track previous values, etc.), you may EXTEND it with an interface (never replace it):
|
|
1256
|
+
*
|
|
1257
|
+
* 1. Define an extended interface BEFORE the projection:
|
|
1258
|
+
* interface InternalWorkoutView extends WorkoutView {
|
|
1259
|
+
* internalField: SomeType;
|
|
1260
|
+
* }
|
|
1261
|
+
*
|
|
1262
|
+
* 2. Assign document to extended type:
|
|
1263
|
+
* const current: InternalWorkoutView = document ?? { ...defaults };
|
|
1264
|
+
*
|
|
1265
|
+
* 3. Return via typed variable (preserves internal state for next event):
|
|
1266
|
+
* const result: InternalWorkoutView = { ...allFields, internalField };
|
|
1267
|
+
* return result;
|
|
1268
|
+
*
|
|
1269
|
+
* This keeps internal state separate from the public GraphQL schema.
|
|
1270
|
+
|
|
1271
|
+
* Event (WorkoutCreated) fields: id: string, name: string
|
|
1272
|
+
*/
|
|
1273
|
+
return {
|
|
1274
|
+
...document,
|
|
1275
|
+
id: /* TODO: map from event.data */ '',
|
|
1276
|
+
name: /* TODO: map from event.data */ '',
|
|
1277
|
+
exercises: /* TODO: map from event.data */ [],
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
case 'ExerciseLogged': {
|
|
1282
|
+
/**
|
|
1283
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
1284
|
+
*
|
|
1285
|
+
* Derive ALL field values from event.data or existing document state.
|
|
1286
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
1287
|
+
* Preserve all import paths above — they are generated from the model.
|
|
1288
|
+
* ⚠️ \`document\` may be null (first event for this entity). Guard before accessing properties.
|
|
1289
|
+
*
|
|
1290
|
+
* CONSTRAINTS:
|
|
1291
|
+
* - NEVER use \`as SomeType\` type assertions. Declare typed variables instead: \`const x: Type = value;\`
|
|
1292
|
+
* - Only reference event.data fields listed in the "Event fields:" line below.
|
|
1293
|
+
* - Do NOT modify anything outside this case block: imports, type parameters, canHandle, collectionName, and getDocumentId are auto-generated.
|
|
1294
|
+
*
|
|
1295
|
+
* Implement how this event updates the projection.
|
|
1296
|
+
*
|
|
1297
|
+
* **Internal State Pattern (extends, never replaces the imported type):**
|
|
1298
|
+
* If you need to track state beyond the public WorkoutView type (e.g., to calculate
|
|
1299
|
+
* aggregations, track previous values, etc.), you may EXTEND it with an interface (never replace it):
|
|
1300
|
+
*
|
|
1301
|
+
* 1. Define an extended interface BEFORE the projection:
|
|
1302
|
+
* interface InternalWorkoutView extends WorkoutView {
|
|
1303
|
+
* internalField: SomeType;
|
|
1304
|
+
* }
|
|
1305
|
+
*
|
|
1306
|
+
* 2. Assign document to extended type:
|
|
1307
|
+
* const current: InternalWorkoutView = document ?? { ...defaults };
|
|
1308
|
+
*
|
|
1309
|
+
* 3. Return via typed variable (preserves internal state for next event):
|
|
1310
|
+
* const result: InternalWorkoutView = { ...allFields, internalField };
|
|
1311
|
+
* return result;
|
|
1312
|
+
*
|
|
1313
|
+
* This keeps internal state separate from the public GraphQL schema.
|
|
1314
|
+
|
|
1315
|
+
* Event (ExerciseLogged) fields: workoutId: string, exercise: string
|
|
1316
|
+
*/
|
|
1317
|
+
return {
|
|
1318
|
+
...document,
|
|
1319
|
+
id: /* TODO: map from event.data */ '',
|
|
1320
|
+
name: /* TODO: map from event.data */ '',
|
|
1321
|
+
exercises: /* TODO: map from event.data */ [],
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
default:
|
|
1325
|
+
return document;
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
export default projection;
|
|
1331
|
+
"
|
|
1332
|
+
`);
|
|
1333
|
+
});
|
|
1011
1334
|
});
|
|
@@ -75,22 +75,45 @@ if (isSingleton) {
|
|
|
75
75
|
index === 0 ? `\${event.data.${field}}` : `-\${event.data.${field}}`
|
|
76
76
|
).join('');
|
|
77
77
|
%>`<%= template %>`<%
|
|
78
|
+
} else if (eventIdFieldMap) {
|
|
79
|
+
const uniqueFields = [...new Set(eventIdFieldMap.values())];
|
|
80
|
+
if (uniqueFields.length === 1) {
|
|
81
|
+
%>event.data.<%= uniqueFields[0] %><%
|
|
82
|
+
} else {
|
|
83
|
+
%>{
|
|
84
|
+
switch (event.type) {
|
|
85
|
+
<% for (const [eventType, field] of eventIdFieldMap) { %>
|
|
86
|
+
case '<%= eventType %>': return event.data.<%= field %>;
|
|
87
|
+
<% } %>
|
|
88
|
+
default: return event.data.<%= typeof idField === 'string' ? idField : 'id' %>;
|
|
89
|
+
}
|
|
90
|
+
}<%
|
|
91
|
+
}
|
|
78
92
|
} else {
|
|
79
93
|
const singleIdField = typeof idField === 'string' ? idField : 'id';
|
|
80
94
|
%>event.data.<%= singleIdField %><%
|
|
81
95
|
}
|
|
82
96
|
%>,
|
|
83
97
|
<%
|
|
84
|
-
|
|
98
|
+
const effectiveIdField = eventIdFieldMap
|
|
99
|
+
? [...new Set(eventIdFieldMap.values())].length === 1 ? [...new Set(eventIdFieldMap.values())][0] : null
|
|
100
|
+
: typeof idField === 'string' ? idField : null;
|
|
101
|
+
if (!isSingleton && !isCompositeKey && effectiveIdField) {
|
|
85
102
|
const missing = events.filter(e => {
|
|
103
|
+
if (eventIdFieldMap) {
|
|
104
|
+
const mappedField = eventIdFieldMap.get(e.type);
|
|
105
|
+
if (!mappedField) return false;
|
|
106
|
+
const def = messages.find(m => m.name === e.type);
|
|
107
|
+
return def && !def.fields?.some(f => f.name === mappedField);
|
|
108
|
+
}
|
|
86
109
|
const def = messages.find(m => m.name === e.type);
|
|
87
|
-
return def && !def.fields?.some(f => f.name ===
|
|
110
|
+
return def && !def.fields?.some(f => f.name === effectiveIdField);
|
|
88
111
|
});
|
|
89
112
|
if (missing.length > 0) {
|
|
90
113
|
%>
|
|
91
|
-
// WARNING: These events lack field '<%=
|
|
114
|
+
// WARNING: These events lack field '<%= effectiveIdField %>': <%= missing.map(e => e.type).join(', ') %>
|
|
92
115
|
// The projection cannot route these events to the correct document.
|
|
93
|
-
// Fix: add '<%=
|
|
116
|
+
// Fix: add '<%= effectiveIdField %>' to these events in the model.
|
|
94
117
|
<% }
|
|
95
118
|
}
|
|
96
119
|
-%>
|