@auto-engineer/server-generator-apollo-emmett 1.131.0 → 1.135.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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +151 -0
  5. package/dist/src/codegen/extract/data-sink.d.ts +1 -0
  6. package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
  7. package/dist/src/codegen/extract/data-sink.js +9 -0
  8. package/dist/src/codegen/extract/data-sink.js.map +1 -1
  9. package/dist/src/codegen/extract/projection.d.ts +6 -1
  10. package/dist/src/codegen/extract/projection.d.ts.map +1 -1
  11. package/dist/src/codegen/extract/projection.js +17 -0
  12. package/dist/src/codegen/extract/projection.js.map +1 -1
  13. package/dist/src/codegen/extract/query.d.ts +8 -2
  14. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  15. package/dist/src/codegen/extract/query.js +36 -0
  16. package/dist/src/codegen/extract/query.js.map +1 -1
  17. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  18. package/dist/src/codegen/scaffoldFromSchema.js +9 -2
  19. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  20. package/dist/src/codegen/templates/query/projection.specs.ts +323 -0
  21. package/dist/src/codegen/templates/query/projection.ts.ejs +27 -4
  22. package/dist/src/codegen/templates/query/query.resolver.specs.ts +242 -0
  23. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
  24. package/dist/src/commands/generate-server.js +5 -5
  25. package/dist/src/commands/generate-server.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +4 -4
  28. package/src/codegen/extract/data-sink.specs.ts +24 -0
  29. package/src/codegen/extract/data-sink.ts +10 -0
  30. package/src/codegen/extract/projection.specs.ts +212 -0
  31. package/src/codegen/extract/projection.ts +22 -1
  32. package/src/codegen/extract/query.specs.ts +137 -0
  33. package/src/codegen/extract/query.ts +46 -2
  34. package/src/codegen/scaffoldFromSchema.ts +13 -0
  35. package/src/codegen/templates/query/projection.specs.ts +323 -0
  36. package/src/codegen/templates/query/projection.ts.ejs +27 -4
  37. package/src/codegen/templates/query/query.resolver.specs.ts +242 -0
  38. package/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
  39. package/src/commands/generate-server.ts +5 -5
@@ -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
- if (!isSingleton && !isCompositeKey && typeof idField === 'string') {
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 === idField);
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 '<%= idField %>': <%= missing.map(e => e.type).join(', ') %>
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 '<%= idField %>' to these events in the model.
116
+ // Fix: add '<%= effectiveIdField %>' to these events in the model.
94
117
  <% }
95
118
  }
96
119
  -%>
@@ -1250,4 +1250,246 @@ describe('query.resolver.ts.ejs', () => {
1250
1250
  expect(resolverFile?.contents).toContain("@Arg('limit', () => Float, { nullable: true }) _limit?: number");
1251
1251
  expect(resolverFile?.contents).not.toContain('ListWorkoutsFilterInput');
1252
1252
  });
1253
+
1254
+ it('should map query arg to state field via value matching when names differ', async () => {
1255
+ const spec: SpecsSchema = {
1256
+ variant: 'specs',
1257
+ narratives: [
1258
+ {
1259
+ name: 'workout-flow',
1260
+ slices: [
1261
+ {
1262
+ type: 'command',
1263
+ name: 'create-workout',
1264
+ stream: 'workouts-${id}',
1265
+ client: { specs: [] },
1266
+ server: {
1267
+ description: '',
1268
+ specs: [
1269
+ {
1270
+ type: 'gherkin',
1271
+ feature: 'Create workout',
1272
+ rules: [
1273
+ {
1274
+ name: 'rule',
1275
+ examples: [
1276
+ {
1277
+ name: 'success',
1278
+ steps: [
1279
+ { keyword: 'When', text: 'CreateWorkout', docString: { id: 'w1', name: 'Leg Day' } },
1280
+ { keyword: 'Then', text: 'WorkoutCreated', docString: { id: 'w1', name: 'Leg Day' } },
1281
+ ],
1282
+ },
1283
+ ],
1284
+ },
1285
+ ],
1286
+ },
1287
+ ],
1288
+ },
1289
+ },
1290
+ {
1291
+ type: 'query',
1292
+ name: 'view-workout',
1293
+ request: `
1294
+ query GetWorkout($workoutId: ID!) {
1295
+ workout(workoutId: $workoutId) {
1296
+ id
1297
+ name
1298
+ }
1299
+ }
1300
+ `,
1301
+ stream: 'workouts',
1302
+ client: { specs: [] },
1303
+ server: {
1304
+ description: '',
1305
+ data: {
1306
+ items: [
1307
+ {
1308
+ target: { type: 'State', name: 'WorkoutView' },
1309
+ origin: { type: 'projection', name: 'WorkoutProjection', idField: 'id' },
1310
+ },
1311
+ ],
1312
+ },
1313
+ specs: [
1314
+ {
1315
+ type: 'gherkin',
1316
+ feature: 'View workout',
1317
+ rules: [
1318
+ {
1319
+ name: 'rule',
1320
+ examples: [
1321
+ {
1322
+ name: 'fetch workout by id',
1323
+ steps: [
1324
+ {
1325
+ keyword: 'When',
1326
+ text: 'GetWorkout',
1327
+ docString: { workoutId: 'w1' },
1328
+ },
1329
+ {
1330
+ keyword: 'Then',
1331
+ text: 'WorkoutView',
1332
+ docString: { id: 'w1', name: 'Leg Day' },
1333
+ },
1334
+ ],
1335
+ },
1336
+ ],
1337
+ },
1338
+ ],
1339
+ },
1340
+ ],
1341
+ },
1342
+ },
1343
+ ],
1344
+ },
1345
+ ],
1346
+ messages: [
1347
+ {
1348
+ type: 'command',
1349
+ name: 'CreateWorkout',
1350
+ fields: [
1351
+ { name: 'id', type: 'string', required: true },
1352
+ { name: 'name', type: 'string', required: true },
1353
+ ],
1354
+ },
1355
+ {
1356
+ type: 'event',
1357
+ name: 'WorkoutCreated',
1358
+ source: 'internal',
1359
+ fields: [
1360
+ { name: 'id', type: 'string', required: true },
1361
+ { name: 'name', type: 'string', required: true },
1362
+ ],
1363
+ },
1364
+ {
1365
+ type: 'state',
1366
+ name: 'WorkoutView',
1367
+ fields: [
1368
+ { name: 'id', type: 'string', required: true },
1369
+ { name: 'name', type: 'string', required: true },
1370
+ ],
1371
+ },
1372
+ ],
1373
+ };
1374
+
1375
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
1376
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('view-workout/query.resolver.ts'));
1377
+
1378
+ expect(resolverFile?.contents).toMatchInlineSnapshot(`
1379
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID } from 'type-graphql';
1380
+ import { type GraphQLContext, ReadModel } from '../../../shared';
1381
+
1382
+ @ObjectType()
1383
+ export class WorkoutView {
1384
+ @Field(() => String)
1385
+ id!: string;
1386
+
1387
+ @Field(() => String)
1388
+ name!: string;
1389
+
1390
+ // IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
1391
+ [key: string]: unknown;
1392
+ }
1393
+
1394
+ @Resolver()
1395
+ export class ViewWorkoutQueryResolver {
1396
+ @Query(() => [WorkoutView])
1397
+ async workout(
1398
+ @Ctx() ctx: GraphQLContext,
1399
+ @Arg('workoutId', () => ID, { nullable: true }) workoutId?: string,
1400
+ ): Promise<WorkoutView[]> {
1401
+ const model = new ReadModel<WorkoutView>(ctx.database, 'WorkoutProjection');
1402
+
1403
+ // ReadModel API:
1404
+ // - model.find(filterFn?) → T[] returns all matches (or all documents if no filter)
1405
+ // - model.findOne(filterFn) → T | null returns the first match
1406
+ //
1407
+ // The scaffolded code below uses find() returning an array.
1408
+ // If this query should return a single item, switch to findOne().
1409
+
1410
+ return model.find((item) => {
1411
+ if (workoutId !== undefined && item.id !== workoutId) return false;
1412
+
1413
+ return true;
1414
+ });
1415
+ }
1416
+ }
1417
+ "
1418
+ `);
1419
+ });
1420
+
1421
+ it('should generate operator-based filters from explicit mappings', async () => {
1422
+ const spec: SpecsSchema = {
1423
+ variant: 'specs',
1424
+ narratives: [
1425
+ {
1426
+ name: 'rental-flow',
1427
+ slices: [
1428
+ {
1429
+ type: 'query',
1430
+ name: 'search-listings',
1431
+ request: `
1432
+ query SearchListings($minBedrooms: Int, $maxPrice: Float) {
1433
+ searchListings(minBedrooms: $minBedrooms, maxPrice: $maxPrice) {
1434
+ listingId
1435
+ numBedrooms
1436
+ pricePerNight
1437
+ }
1438
+ }
1439
+ `,
1440
+ mappings: [
1441
+ {
1442
+ source: { type: 'Query', name: 'SearchListings', field: 'minBedrooms' },
1443
+ target: { type: 'State', name: 'ListingResult', field: 'numBedrooms' },
1444
+ operator: 'gte',
1445
+ },
1446
+ {
1447
+ source: { type: 'Query', name: 'SearchListings', field: 'maxPrice' },
1448
+ target: { type: 'State', name: 'ListingResult', field: 'pricePerNight' },
1449
+ operator: 'lte',
1450
+ },
1451
+ ],
1452
+ client: { specs: [] },
1453
+ server: {
1454
+ description: '',
1455
+ data: {
1456
+ items: [
1457
+ {
1458
+ origin: {
1459
+ type: 'projection',
1460
+ idField: 'listingId',
1461
+ name: 'ListingsProjection',
1462
+ },
1463
+ target: {
1464
+ type: 'State',
1465
+ name: 'ListingResult',
1466
+ },
1467
+ },
1468
+ ],
1469
+ },
1470
+ specs: [],
1471
+ },
1472
+ },
1473
+ ],
1474
+ },
1475
+ ],
1476
+ messages: [
1477
+ {
1478
+ type: 'state',
1479
+ name: 'ListingResult',
1480
+ fields: [
1481
+ { name: 'listingId', type: 'string', required: true },
1482
+ { name: 'numBedrooms', type: 'number', required: true },
1483
+ { name: 'pricePerNight', type: 'number', required: true },
1484
+ ],
1485
+ },
1486
+ ],
1487
+ };
1488
+
1489
+ const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
1490
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
1491
+
1492
+ expect(resolverFile?.contents).toContain('item.numBedrooms < minBedrooms');
1493
+ expect(resolverFile?.contents).toContain('item.pricePerNight > maxPrice');
1494
+ });
1253
1495
  });
@@ -37,9 +37,11 @@ for (const field of messageFields) {
37
37
  const hasArgs = parsedRequest?.args?.length > 0;
38
38
 
39
39
  const stateFieldNames = new Set(messageFields.map(f => f.name));
40
+ const argFieldMap = argToStateFieldMap ?? {};
41
+ const negatedOp = { eq: '!==', ne: '===', gt: '<=', gte: '<', lt: '>=', lte: '>' };
40
42
  const usedArgNames = isSingleton
41
43
  ? new Set()
42
- : new Set((parsedRequest?.args ?? []).filter(a => stateFieldNames.has(a.name)).map(a => a.name));
44
+ : new Set((parsedRequest?.args ?? []).filter(a => stateFieldNames.has(a.name) || argFieldMap[a.name]).map(a => a.name));
43
45
  const hasMatchingArgs = usedArgNames.size > 0;
44
46
 
45
47
  const resolveArgTypes = (arg) => {
@@ -180,10 +182,12 @@ const model = new ReadModel<<%= viewType %>>(ctx.database, '<%= collectionName %
180
182
 
181
183
  return model.find((<%= hasMatchingArgs ? 'item' : '_item' %>) => {
182
184
  <% if (parsedRequest?.args?.length) {
183
- const stateFieldNames = new Set(messageFields.map(f => f.name));
184
185
  for (const arg of parsedRequest.args) {
185
- if (stateFieldNames.has(arg.name)) { %>
186
- if (<%= arg.name %> !== undefined && item.<%= arg.name %> !== <%= arg.name %>) return false;
186
+ const mapping = argFieldMap[arg.name];
187
+ const mappedField = mapping ? mapping.field : (stateFieldNames.has(arg.name) ? arg.name : null);
188
+ const op = mapping ? negatedOp[mapping.operator] : '!==';
189
+ if (mappedField) { %>
190
+ if (<%= arg.name %> !== undefined && item.<%= mappedField %> <%= op %> <%= arg.name %>) return false;
187
191
  <% } else { %>
188
192
  // NOTE: '<%= arg.name %>' has no matching field on the state type — add custom filter logic if needed.
189
193
  <% }
@@ -117,14 +117,14 @@ export type GenerateServerEvents =
117
117
 
118
118
  export const commandHandler = defineCommandHandler({
119
119
  name: 'GenerateServer',
120
- displayName: 'Generate Server',
121
- alias: 'generate:server',
120
+ displayName: 'Build Back End',
121
+ alias: 'build:backend',
122
122
  description: 'Generate server from model',
123
123
  category: 'generate',
124
124
  icon: 'server',
125
125
  events: [
126
- { name: 'ServerGenerated', displayName: 'Server Generated' },
127
- { name: 'ServerGenerationFailed', displayName: 'Server Generation Failed' },
126
+ { name: 'ServerGenerated', displayName: 'Back End Built' },
127
+ { name: 'ServerGenerationFailed', displayName: 'Back End Build Failed' },
128
128
  { name: 'SliceGenerated', displayName: 'Slice Generated' },
129
129
  { name: 'SliceGenerationFailed', displayName: 'Slice Generation Failed' },
130
130
  ],
@@ -138,7 +138,7 @@ export const commandHandler = defineCommandHandler({
138
138
  required: true,
139
139
  },
140
140
  },
141
- examples: ['$ auto generate:server --destination=.'],
141
+ examples: ['$ auto build:backend --destination=.'],
142
142
  handle: async (command: Command): Promise<GenerateServerEvents | GenerateServerEvents[]> => {
143
143
  const typedCommand = command as GenerateServerCommand;
144
144
  const result = await handleGenerateServerCommandInternal(typedCommand);