@auto-engineer/server-generator-apollo-emmett 1.88.0 → 1.90.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 +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +90 -0
- package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
- package/dist/src/codegen/extract/slice-normalizer.js +14 -0
- package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +17 -0
- package/dist/src/codegen/extract/type-helpers.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +6 -4
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +293 -34
- package/dist/src/codegen/templates/command/decide.specs.ts +34 -14
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
- package/dist/src/codegen/templates/command/decide.ts.ejs +32 -4
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +124 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
- package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/dist/src/codegen/templates/react/react.specs.ts +9 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
- package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/dist/src/codegen/templates/react/register.specs.ts +27 -23
- package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +12 -3
- package/package.json +4 -4
- package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
- package/src/codegen/extract/slice-normalizer.ts +15 -0
- package/src/codegen/extract/type-helpers.specs.ts +77 -1
- package/src/codegen/extract/type-helpers.ts +23 -0
- package/src/codegen/formatTsValueSimple.specs.ts +8 -0
- package/src/codegen/scaffoldFromSchema.ts +7 -3
- package/src/codegen/templates/command/decide.specs.specs.ts +293 -34
- package/src/codegen/templates/command/decide.specs.ts +34 -14
- package/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
- package/src/codegen/templates/command/decide.ts.ejs +32 -4
- package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/src/codegen/templates/query/projection.specs.specs.ts +124 -0
- package/src/codegen/templates/query/projection.specs.ts +20 -0
- package/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
- package/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/src/codegen/templates/react/react.specs.ts +9 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/src/codegen/templates/react/react.ts.ejs +22 -9
- package/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/src/codegen/templates/react/register.specs.ts +27 -23
- package/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/src/codegen/extract/graphql.d.ts +0 -14
- package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
- package/dist/src/codegen/extract/graphql.js +0 -81
- package/dist/src/codegen/extract/graphql.js.map +0 -1
- package/src/codegen/extract/graphql.ts +0 -103
|
@@ -117,6 +117,8 @@ describe('spec.ts.ejs', () => {
|
|
|
117
117
|
initialState,
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
121
|
+
|
|
120
122
|
it('User creates listing with valid data', () => {
|
|
121
123
|
given([])
|
|
122
124
|
.when({
|
|
@@ -136,8 +138,8 @@ describe('spec.ts.ejs', () => {
|
|
|
136
138
|
metadata: { now: new Date() },
|
|
137
139
|
})
|
|
138
140
|
|
|
139
|
-
.then(
|
|
140
|
-
{
|
|
141
|
+
.then(
|
|
142
|
+
expectEvents({
|
|
141
143
|
type: 'ListingCreated',
|
|
142
144
|
data: {
|
|
143
145
|
propertyId: 'listing_123',
|
|
@@ -145,8 +147,8 @@ describe('spec.ts.ejs', () => {
|
|
|
145
147
|
rating: 4.8,
|
|
146
148
|
metadata: { foo: 'bar' },
|
|
147
149
|
},
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
150
152
|
});
|
|
151
153
|
});
|
|
152
154
|
"
|
|
@@ -263,6 +265,8 @@ describe('spec.ts.ejs', () => {
|
|
|
263
265
|
initialState,
|
|
264
266
|
});
|
|
265
267
|
|
|
268
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
269
|
+
|
|
266
270
|
it('Existing listing can be removed', () => {
|
|
267
271
|
given([
|
|
268
272
|
{
|
|
@@ -283,15 +287,14 @@ describe('spec.ts.ejs', () => {
|
|
|
283
287
|
metadata: { now: new Date() },
|
|
284
288
|
})
|
|
285
289
|
|
|
286
|
-
.then(
|
|
287
|
-
{
|
|
290
|
+
.then(
|
|
291
|
+
expectEvents({
|
|
288
292
|
type: 'ListingRemoved',
|
|
289
293
|
data: {
|
|
290
294
|
propertyId: 'listing_123',
|
|
291
|
-
removedAt: new Date('2024-01-16T10:00:00Z'),
|
|
292
295
|
},
|
|
293
|
-
},
|
|
294
|
-
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
295
298
|
});
|
|
296
299
|
});
|
|
297
300
|
"
|
|
@@ -457,6 +460,8 @@ describe('spec.ts.ejs', () => {
|
|
|
457
460
|
initialState,
|
|
458
461
|
});
|
|
459
462
|
|
|
463
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
464
|
+
|
|
460
465
|
it('no questions have been answered yet', () => {
|
|
461
466
|
given([])
|
|
462
467
|
.when({
|
|
@@ -470,18 +475,17 @@ describe('spec.ts.ejs', () => {
|
|
|
470
475
|
metadata: { now: new Date() },
|
|
471
476
|
})
|
|
472
477
|
|
|
473
|
-
.then(
|
|
474
|
-
{
|
|
478
|
+
.then(
|
|
479
|
+
expectEvents({
|
|
475
480
|
type: 'QuestionAnswered',
|
|
476
481
|
data: {
|
|
477
482
|
questionnaireId: 'q-001',
|
|
478
483
|
participantId: 'participant-abc',
|
|
479
484
|
questionId: 'q1',
|
|
480
485
|
answer: 'Yes',
|
|
481
|
-
savedAt: new Date('2030-01-01T09:05:00.000Z'),
|
|
482
486
|
},
|
|
483
|
-
},
|
|
484
|
-
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
485
489
|
});
|
|
486
490
|
|
|
487
491
|
it('all questions have already been answered and submitted', () => {
|
|
@@ -506,17 +510,15 @@ describe('spec.ts.ejs', () => {
|
|
|
506
510
|
metadata: { now: new Date() },
|
|
507
511
|
})
|
|
508
512
|
|
|
509
|
-
.then(
|
|
510
|
-
{
|
|
513
|
+
.then(
|
|
514
|
+
expectEvents({
|
|
511
515
|
type: 'QuestionnaireEditRejected',
|
|
512
516
|
data: {
|
|
513
517
|
questionnaireId: 'q-001',
|
|
514
518
|
participantId: 'participant-abc',
|
|
515
|
-
reason: 'Questionnaire already submitted',
|
|
516
|
-
attemptedAt: new Date('2030-01-01T09:05:00.000Z'),
|
|
517
519
|
},
|
|
518
|
-
},
|
|
519
|
-
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
520
522
|
});
|
|
521
523
|
});
|
|
522
524
|
"
|
|
@@ -721,6 +723,8 @@ describe('spec.ts.ejs', () => {
|
|
|
721
723
|
initialState,
|
|
722
724
|
});
|
|
723
725
|
|
|
726
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) => events as Events[];
|
|
727
|
+
|
|
724
728
|
it('Workout submitted updates stats', () => {
|
|
725
729
|
given([])
|
|
726
730
|
.when({
|
|
@@ -732,22 +736,22 @@ describe('spec.ts.ejs', () => {
|
|
|
732
736
|
metadata: { now: new Date() },
|
|
733
737
|
})
|
|
734
738
|
|
|
735
|
-
.then(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
739
|
+
.then(
|
|
740
|
+
expectEvents(
|
|
741
|
+
{
|
|
742
|
+
type: 'StatsUpdated',
|
|
743
|
+
data: {
|
|
744
|
+
workoutId: 'w1',
|
|
745
|
+
},
|
|
741
746
|
},
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
loggedAt: new Date('2024-01-01T00:00:00Z'),
|
|
747
|
+
{
|
|
748
|
+
type: 'WorkoutLogged',
|
|
749
|
+
data: {
|
|
750
|
+
workoutId: 'w1',
|
|
751
|
+
},
|
|
748
752
|
},
|
|
749
|
-
|
|
750
|
-
|
|
753
|
+
),
|
|
754
|
+
);
|
|
751
755
|
});
|
|
752
756
|
});
|
|
753
757
|
"
|
|
@@ -1128,4 +1132,259 @@ describe('spec.ts.ejs', () => {
|
|
|
1128
1132
|
expect(specFile?.contents).not.toContain("new Date('2024-01-20')");
|
|
1129
1133
|
expect(specFile?.contents).not.toContain("new Date('2024-06-30')");
|
|
1130
1134
|
});
|
|
1135
|
+
|
|
1136
|
+
it('should omit non-command fields from Then assertions (Emmett subset matching)', async () => {
|
|
1137
|
+
const spec: SpecsSchema = {
|
|
1138
|
+
variant: 'specs',
|
|
1139
|
+
narratives: [
|
|
1140
|
+
{
|
|
1141
|
+
name: 'Fitness flow',
|
|
1142
|
+
slices: [
|
|
1143
|
+
{
|
|
1144
|
+
type: 'command',
|
|
1145
|
+
name: 'Log workout',
|
|
1146
|
+
client: { specs: [] },
|
|
1147
|
+
server: {
|
|
1148
|
+
description: '',
|
|
1149
|
+
specs: [
|
|
1150
|
+
{
|
|
1151
|
+
type: 'gherkin',
|
|
1152
|
+
feature: 'Log workout',
|
|
1153
|
+
rules: [
|
|
1154
|
+
{
|
|
1155
|
+
name: 'Should log workout',
|
|
1156
|
+
examples: [
|
|
1157
|
+
{
|
|
1158
|
+
name: 'Workout logged successfully',
|
|
1159
|
+
steps: [
|
|
1160
|
+
{
|
|
1161
|
+
keyword: 'When',
|
|
1162
|
+
text: 'LogWorkout',
|
|
1163
|
+
docString: {
|
|
1164
|
+
memberId: 'mem_001',
|
|
1165
|
+
date: '2024-01-15',
|
|
1166
|
+
exercises: ['bench press'],
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
keyword: 'Then',
|
|
1171
|
+
text: 'WorkoutLogged',
|
|
1172
|
+
docString: {
|
|
1173
|
+
workoutId: 'wkt_456',
|
|
1174
|
+
memberId: 'mem_001',
|
|
1175
|
+
date: '2024-01-15',
|
|
1176
|
+
exercises: ['bench press'],
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
],
|
|
1180
|
+
},
|
|
1181
|
+
],
|
|
1182
|
+
},
|
|
1183
|
+
],
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
],
|
|
1189
|
+
},
|
|
1190
|
+
],
|
|
1191
|
+
messages: [
|
|
1192
|
+
{
|
|
1193
|
+
type: 'command',
|
|
1194
|
+
name: 'LogWorkout',
|
|
1195
|
+
fields: [
|
|
1196
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
1197
|
+
{ name: 'date', type: 'string', required: true },
|
|
1198
|
+
{ name: 'exercises', type: 'string[]', required: true },
|
|
1199
|
+
],
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
type: 'event',
|
|
1203
|
+
name: 'WorkoutLogged',
|
|
1204
|
+
source: 'internal',
|
|
1205
|
+
fields: [
|
|
1206
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
1207
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
1208
|
+
{ name: 'date', type: 'string', required: true },
|
|
1209
|
+
{ name: 'exercises', type: 'string[]', required: true },
|
|
1210
|
+
],
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1216
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1217
|
+
|
|
1218
|
+
expect(specFile?.contents).not.toContain('workoutId:');
|
|
1219
|
+
expect(specFile?.contents).toContain("memberId: 'mem_001'");
|
|
1220
|
+
expect(specFile?.contents).toContain("date: '2024-01-15'");
|
|
1221
|
+
expect(specFile?.contents).toContain("exercises: ['bench press']");
|
|
1222
|
+
expect(specFile?.contents).toContain('const expectEvents');
|
|
1223
|
+
expect(specFile?.contents).toContain('events as Events[]');
|
|
1224
|
+
expect(specFile?.contents).toContain('.then(\n');
|
|
1225
|
+
expect(specFile?.contents).toContain('expectEvents(');
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it('should keep non-command fields whose key+value match a Given event', async () => {
|
|
1229
|
+
const spec: SpecsSchema = {
|
|
1230
|
+
variant: 'specs',
|
|
1231
|
+
narratives: [
|
|
1232
|
+
{
|
|
1233
|
+
name: 'Subscription flow',
|
|
1234
|
+
slices: [
|
|
1235
|
+
{
|
|
1236
|
+
type: 'command',
|
|
1237
|
+
name: 'Renew subscription',
|
|
1238
|
+
client: { specs: [] },
|
|
1239
|
+
server: {
|
|
1240
|
+
description: '',
|
|
1241
|
+
specs: [
|
|
1242
|
+
{
|
|
1243
|
+
type: 'gherkin',
|
|
1244
|
+
feature: 'Renew subscription',
|
|
1245
|
+
rules: [
|
|
1246
|
+
{
|
|
1247
|
+
name: 'Should renew',
|
|
1248
|
+
examples: [
|
|
1249
|
+
{
|
|
1250
|
+
name: 'Renewal preserves total points from state',
|
|
1251
|
+
steps: [
|
|
1252
|
+
{
|
|
1253
|
+
keyword: 'Given',
|
|
1254
|
+
text: 'SubscriptionCreated',
|
|
1255
|
+
docString: { subId: 's1', totalPoints: 100 },
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
keyword: 'When',
|
|
1259
|
+
text: 'RenewSubscription',
|
|
1260
|
+
docString: { subId: 's1' },
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
keyword: 'Then',
|
|
1264
|
+
text: 'SubscriptionRenewed',
|
|
1265
|
+
docString: { subId: 's1', totalPoints: 100 },
|
|
1266
|
+
},
|
|
1267
|
+
],
|
|
1268
|
+
},
|
|
1269
|
+
],
|
|
1270
|
+
},
|
|
1271
|
+
],
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
],
|
|
1277
|
+
},
|
|
1278
|
+
],
|
|
1279
|
+
messages: [
|
|
1280
|
+
{
|
|
1281
|
+
type: 'command',
|
|
1282
|
+
name: 'RenewSubscription',
|
|
1283
|
+
fields: [{ name: 'subId', type: 'string', required: true }],
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
type: 'event',
|
|
1287
|
+
name: 'SubscriptionCreated',
|
|
1288
|
+
source: 'internal',
|
|
1289
|
+
fields: [
|
|
1290
|
+
{ name: 'subId', type: 'string', required: true },
|
|
1291
|
+
{ name: 'totalPoints', type: 'number', required: true },
|
|
1292
|
+
],
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
type: 'event',
|
|
1296
|
+
name: 'SubscriptionRenewed',
|
|
1297
|
+
source: 'internal',
|
|
1298
|
+
fields: [
|
|
1299
|
+
{ name: 'subId', type: 'string', required: true },
|
|
1300
|
+
{ name: 'totalPoints', type: 'number', required: true },
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1307
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1308
|
+
|
|
1309
|
+
expect(specFile?.contents).toContain('totalPoints: 100');
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it('should list non-command fields in decide scaffold comments', async () => {
|
|
1313
|
+
const spec: SpecsSchema = {
|
|
1314
|
+
variant: 'specs',
|
|
1315
|
+
narratives: [
|
|
1316
|
+
{
|
|
1317
|
+
name: 'Fitness flow',
|
|
1318
|
+
slices: [
|
|
1319
|
+
{
|
|
1320
|
+
type: 'command',
|
|
1321
|
+
name: 'Log workout',
|
|
1322
|
+
client: { specs: [] },
|
|
1323
|
+
server: {
|
|
1324
|
+
description: '',
|
|
1325
|
+
specs: [
|
|
1326
|
+
{
|
|
1327
|
+
type: 'gherkin',
|
|
1328
|
+
feature: 'Log workout',
|
|
1329
|
+
rules: [
|
|
1330
|
+
{
|
|
1331
|
+
name: 'Should log workout',
|
|
1332
|
+
examples: [
|
|
1333
|
+
{
|
|
1334
|
+
name: 'Workout logged successfully',
|
|
1335
|
+
steps: [
|
|
1336
|
+
{
|
|
1337
|
+
keyword: 'When',
|
|
1338
|
+
text: 'LogWorkout',
|
|
1339
|
+
docString: { memberId: 'mem_001', date: '2024-01-15' },
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
keyword: 'Then',
|
|
1343
|
+
text: 'WorkoutLogged',
|
|
1344
|
+
docString: { workoutId: 'wkt_456', memberId: 'mem_001', date: '2024-01-15' },
|
|
1345
|
+
},
|
|
1346
|
+
],
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
},
|
|
1350
|
+
],
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1356
|
+
},
|
|
1357
|
+
],
|
|
1358
|
+
messages: [
|
|
1359
|
+
{
|
|
1360
|
+
type: 'command',
|
|
1361
|
+
name: 'LogWorkout',
|
|
1362
|
+
fields: [
|
|
1363
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
1364
|
+
{ name: 'date', type: 'string', required: true },
|
|
1365
|
+
],
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
type: 'event',
|
|
1369
|
+
name: 'WorkoutLogged',
|
|
1370
|
+
source: 'internal',
|
|
1371
|
+
fields: [
|
|
1372
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
1373
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
1374
|
+
{ name: 'date', type: 'string', required: true },
|
|
1375
|
+
],
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1381
|
+
const decideFile = plans.find((p) => p.outputPath.endsWith('decide.ts'));
|
|
1382
|
+
|
|
1383
|
+
expect(decideFile?.contents).toContain('REQUIRED: Your return value MUST include ALL fields');
|
|
1384
|
+
expect(decideFile?.contents).toContain('Do NOT use');
|
|
1385
|
+
expect(decideFile?.contents).toContain('Fields NOT in command input');
|
|
1386
|
+
expect(decideFile?.contents).toContain('workoutId: string');
|
|
1387
|
+
expect(decideFile?.contents).toContain('...command.data');
|
|
1388
|
+
expect(decideFile?.contents).not.toContain('`NotFoundError`');
|
|
1389
|
+
});
|
|
1131
1390
|
});
|
|
@@ -97,8 +97,8 @@ describe('decide.ts.ejs', () => {
|
|
|
97
97
|
* You should:
|
|
98
98
|
* - Validate the command input fields
|
|
99
99
|
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
100
|
-
* - If invalid, throw one of the following domain errors: \`
|
|
101
|
-
* ⚠️ Error constructors:
|
|
100
|
+
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`
|
|
101
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message
|
|
102
102
|
* - If valid, return one or more events with the correct structure
|
|
103
103
|
*
|
|
104
104
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -107,10 +107,12 @@ describe('decide.ts.ejs', () => {
|
|
|
107
107
|
* - Should create listing with valid data
|
|
108
108
|
*/
|
|
109
109
|
|
|
110
|
+
// All event fields come from command input — use ...command.data to pass them through.
|
|
111
|
+
|
|
110
112
|
// return {
|
|
111
113
|
// type: 'ListingCreated',
|
|
112
114
|
// data: { ...command.data },
|
|
113
|
-
// }
|
|
115
|
+
// };
|
|
114
116
|
|
|
115
117
|
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
116
118
|
}
|
|
@@ -230,8 +232,8 @@ describe('decide.ts.ejs', () => {
|
|
|
230
232
|
* You should:
|
|
231
233
|
* - Validate the command input fields
|
|
232
234
|
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
233
|
-
* - If invalid, throw one of the following domain errors: \`
|
|
234
|
-
* ⚠️ Error constructors:
|
|
235
|
+
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`
|
|
236
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message
|
|
235
237
|
* - If valid, return one or more events with the correct structure
|
|
236
238
|
*
|
|
237
239
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -240,10 +242,18 @@ describe('decide.ts.ejs', () => {
|
|
|
240
242
|
* - Should remove existing listing
|
|
241
243
|
*/
|
|
242
244
|
|
|
245
|
+
// ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
|
|
246
|
+
// Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
|
|
247
|
+
// Do NOT use 'as ListingRemoved' to silence missing fields.
|
|
248
|
+
//
|
|
249
|
+
// Fields from command input → use ...command.data or command.data.<fieldName>
|
|
250
|
+
// Fields NOT in command input → produce dynamically (never hardcode):
|
|
251
|
+
// removedAt: Date — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
|
|
252
|
+
|
|
243
253
|
// return {
|
|
244
254
|
// type: 'ListingRemoved',
|
|
245
|
-
// data: { ...command.data },
|
|
246
|
-
// }
|
|
255
|
+
// data: { ...command.data, /* + dynamically produce: removedAt */ },
|
|
256
|
+
// };
|
|
247
257
|
|
|
248
258
|
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
249
259
|
}
|
|
@@ -383,8 +393,8 @@ describe('decide.ts.ejs', () => {
|
|
|
383
393
|
* You should:
|
|
384
394
|
* - Validate the command input fields
|
|
385
395
|
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
386
|
-
* - If invalid, throw one of the following domain errors: \`
|
|
387
|
-
* ⚠️ Error constructors:
|
|
396
|
+
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`, \`ValidationError\`
|
|
397
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message, ValidationError takes a string message
|
|
388
398
|
* - If valid, return one or more events with the correct structure
|
|
389
399
|
*
|
|
390
400
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -397,10 +407,12 @@ describe('decide.ts.ejs', () => {
|
|
|
397
407
|
throw new ValidationError('Title must not be empty');
|
|
398
408
|
}
|
|
399
409
|
|
|
410
|
+
// All event fields come from command input — use ...command.data to pass them through.
|
|
411
|
+
|
|
400
412
|
// return {
|
|
401
413
|
// type: 'ListingCreated',
|
|
402
414
|
// data: { ...command.data },
|
|
403
|
-
// }
|
|
415
|
+
// };
|
|
404
416
|
|
|
405
417
|
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
406
418
|
}
|
|
@@ -581,8 +593,8 @@ describe('decide.ts.ejs', () => {
|
|
|
581
593
|
* - Validate the command input fields
|
|
582
594
|
* - Inspect the current domain \`_state\` to determine if the command is allowed
|
|
583
595
|
* - Use \`products\` (integration result) to enrich or filter the output
|
|
584
|
-
* - If invalid, throw one of the following domain errors: \`
|
|
585
|
-
* ⚠️ Error constructors:
|
|
596
|
+
* - If invalid, throw one of the following domain errors: \`IllegalStateError\`
|
|
597
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message
|
|
586
598
|
* - If valid, return one or more events with the correct structure
|
|
587
599
|
*
|
|
588
600
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -599,10 +611,18 @@ describe('decide.ts.ejs', () => {
|
|
|
599
611
|
* - Should suggest items successfully
|
|
600
612
|
*/
|
|
601
613
|
|
|
614
|
+
// ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
|
|
615
|
+
// Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
|
|
616
|
+
// Do NOT use 'as ItemsSuggested' to silence missing fields.
|
|
617
|
+
//
|
|
618
|
+
// Fields from command input → use ...command.data or command.data.<fieldName>
|
|
619
|
+
// Fields NOT in command input → produce dynamically (never hardcode):
|
|
620
|
+
// items: Array<object> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
|
|
621
|
+
|
|
602
622
|
// return {
|
|
603
623
|
// type: 'ItemsSuggested',
|
|
604
|
-
// data: { ...command.data },
|
|
605
|
-
// }
|
|
624
|
+
// data: { ...command.data, /* + dynamically produce: items */ },
|
|
625
|
+
// };
|
|
606
626
|
|
|
607
627
|
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
608
628
|
}
|
|
@@ -54,29 +54,50 @@ for (const [importPath, eventTypes] of testEventsByPath.entries()) {
|
|
|
54
54
|
|
|
55
55
|
const uniqueEventTypes = Array.from(new Set(allEvents.map(e => e?.type).filter(Boolean))).sort();
|
|
56
56
|
|
|
57
|
-
function
|
|
58
|
-
const commandFields = new Set(commandSchema?.fields?.map(f => f.name) || []);
|
|
57
|
+
function findDerivedDateInfo(eventResults, commandFieldNames, givenEvents) {
|
|
59
58
|
const givenValues = new Set();
|
|
60
59
|
for (const g of givenEvents || []) {
|
|
61
60
|
for (const val of Object.values(g.exampleData || {})) {
|
|
62
61
|
if (typeof val === 'string') givenValues.add(val);
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
const
|
|
64
|
+
const fieldsByDate = new Map();
|
|
66
65
|
for (const e of eventResults) {
|
|
67
|
-
const
|
|
68
|
-
for (const [key, val] of Object.entries(data)) {
|
|
66
|
+
for (const [key, val] of Object.entries(e.exampleData || {})) {
|
|
69
67
|
if (
|
|
70
|
-
!
|
|
68
|
+
!commandFieldNames.has(key) &&
|
|
71
69
|
typeof val === 'string' &&
|
|
72
70
|
/^\d{4}-\d{2}-\d{2}$/.test(val) &&
|
|
73
71
|
!givenValues.has(val)
|
|
74
72
|
) {
|
|
75
|
-
|
|
73
|
+
if (!fieldsByDate.has(val)) fieldsByDate.set(val, []);
|
|
74
|
+
fieldsByDate.get(val).push(key);
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
}
|
|
79
|
-
|
|
78
|
+
if (fieldsByDate.size !== 1) return { date: null, fields: [] };
|
|
79
|
+
const [date, fields] = [...fieldsByDate.entries()][0];
|
|
80
|
+
return { date, fields };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isKeyTraceable(key, value, givenEvents) {
|
|
84
|
+
if (value === null || value === undefined || typeof value === 'object') return false;
|
|
85
|
+
for (const g of givenEvents || []) {
|
|
86
|
+
if ((g.exampleData || {})[key] === value) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents) {
|
|
92
|
+
const keep = new Set([...commandFieldNames, ...derivedDateFieldNames]);
|
|
93
|
+
for (const e of eventResults) {
|
|
94
|
+
for (const [key, value] of Object.entries(e.exampleData || {})) {
|
|
95
|
+
if (!keep.has(key) && isKeyTraceable(key, value, givenEvents)) {
|
|
96
|
+
keep.add(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return keep;
|
|
80
101
|
}
|
|
81
102
|
_%>
|
|
82
103
|
import { describe, it } from 'vitest';
|
|
@@ -99,6 +120,9 @@ describe('<%= ruleDescription %>', () => {
|
|
|
99
120
|
initialState,
|
|
100
121
|
});
|
|
101
122
|
|
|
123
|
+
const expectEvents = (...events: Array<{ type: string; data: unknown }>) =>
|
|
124
|
+
events as Events[];
|
|
125
|
+
|
|
102
126
|
<% for (const { commandName, gwt } of ruleGwts) {
|
|
103
127
|
const schema = commandSchemasByName[commandName];
|
|
104
128
|
const example = gwt.when;
|
|
@@ -122,19 +146,28 @@ describe('<%= ruleDescription %>', () => {
|
|
|
122
146
|
.when({
|
|
123
147
|
type: '<%= example.commandRef %>',
|
|
124
148
|
data: <%- formatDataObject(example.exampleData, schema) %>,
|
|
125
|
-
<% const
|
|
149
|
+
<% const commandFieldNames = new Set(schema?.fields?.map(f => f.name) || []);
|
|
150
|
+
const { date: derivedDate, fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, commandFieldNames, gwt.given);
|
|
151
|
+
const keepFieldNames = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, gwt.given);
|
|
152
|
+
-%>
|
|
126
153
|
metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
|
|
127
154
|
})
|
|
128
155
|
<% if (errorResult) { %>
|
|
129
156
|
.thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= errorResult.message || '' %>');
|
|
130
157
|
<% } else { %>
|
|
131
158
|
|
|
132
|
-
.then(
|
|
133
|
-
<%- eventResults.map(e =>
|
|
159
|
+
.then(expectEvents(
|
|
160
|
+
<%- eventResults.map(e => {
|
|
161
|
+
const evtSchema = events.find(evt => evt.type === e.eventRef);
|
|
162
|
+
const filteredData = Object.fromEntries(
|
|
163
|
+
Object.entries(e.exampleData || {}).filter(([key]) => keepFieldNames.has(key))
|
|
164
|
+
);
|
|
165
|
+
return `{
|
|
134
166
|
type: '${e.eventRef}',
|
|
135
|
-
data: ${formatDataObject(
|
|
136
|
-
}
|
|
137
|
-
|
|
167
|
+
data: ${formatDataObject(filteredData, evtSchema)}
|
|
168
|
+
}`;
|
|
169
|
+
}).join(',\n ') %>
|
|
170
|
+
));
|
|
138
171
|
<% } %>
|
|
139
172
|
});
|
|
140
173
|
<% } %>
|