@voxgig/apidef 8.13.0 → 8.15.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 (67) hide show
  1. package/bin/voxgig-apidef +1 -174
  2. package/dist/apidef.d.ts +2 -2
  3. package/dist/apidef.js +2 -2
  4. package/dist/apidef.js.map +1 -1
  5. package/dist/builder/entity/entity.d.ts +5 -1
  6. package/dist/builder/entity/entity.js +17 -1
  7. package/dist/builder/entity/entity.js.map +1 -1
  8. package/dist/builder/flow/flowHeuristic01.js +14 -14
  9. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  10. package/dist/cli.d.ts +38 -0
  11. package/dist/cli.js +279 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/guide/guide.js +8 -5
  14. package/dist/guide/guide.js.map +1 -1
  15. package/dist/model.d.ts +35 -33
  16. package/dist/resolved.d.ts +10 -6
  17. package/dist/resolved.js +65 -15
  18. package/dist/resolved.js.map +1 -1
  19. package/dist/transform/args.js +19 -19
  20. package/dist/transform/args.js.map +1 -1
  21. package/dist/transform/clean.js +4 -0
  22. package/dist/transform/clean.js.map +1 -1
  23. package/dist/transform/contract.d.ts +0 -2
  24. package/dist/transform/contract.js +6 -75
  25. package/dist/transform/contract.js.map +1 -1
  26. package/dist/transform/entity.d.ts +2 -1
  27. package/dist/transform/entity.js +13 -1
  28. package/dist/transform/entity.js.map +1 -1
  29. package/dist/transform/field.js +64 -64
  30. package/dist/transform/field.js.map +1 -1
  31. package/dist/transform/flowstep.js +42 -42
  32. package/dist/transform/flowstep.js.map +1 -1
  33. package/dist/transform/graphql.js +8 -8
  34. package/dist/transform/graphql.js.map +1 -1
  35. package/dist/transform/operation.js +9 -9
  36. package/dist/transform/operation.js.map +1 -1
  37. package/dist/transform/select.js +13 -13
  38. package/dist/transform/select.js.map +1 -1
  39. package/dist/transform/top.js +1 -1
  40. package/dist/transform/top.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/types.d.ts +1 -1
  43. package/dist/utility.d.ts +2 -1
  44. package/dist/utility.js +6 -1
  45. package/dist/utility.js.map +1 -1
  46. package/model/apidef.aon +128 -157
  47. package/model/guide.aon +3 -2
  48. package/package.json +5 -3
  49. package/src/apidef.ts +4 -3
  50. package/src/builder/entity/entity.ts +18 -1
  51. package/src/builder/flow/flowHeuristic01.ts +14 -14
  52. package/src/cli.ts +341 -0
  53. package/src/guide/guide.ts +8 -4
  54. package/src/model.ts +36 -37
  55. package/src/resolved.ts +62 -17
  56. package/src/transform/args.ts +20 -20
  57. package/src/transform/clean.ts +5 -0
  58. package/src/transform/contract.ts +6 -71
  59. package/src/transform/entity.ts +13 -1
  60. package/src/transform/field.ts +66 -69
  61. package/src/transform/flowstep.ts +42 -42
  62. package/src/transform/graphql.ts +8 -8
  63. package/src/transform/operation.ts +9 -9
  64. package/src/transform/select.ts +13 -13
  65. package/src/transform/top.ts +1 -1
  66. package/src/types.ts +1 -1
  67. package/src/utility.ts +8 -1
@@ -35,7 +35,8 @@ function resolveEntity(
35
35
  each(kit.entity, ((entity: any, entityName: string) => {
36
36
  const entityFile = (null == opts.outprefix ? '' : opts.outprefix) + entityName + '.aon'
37
37
 
38
- let entityJSONIC = formatJSONIC(entity).trim()
38
+ const { model, relations } = entityAncestorSource(entity)
39
+ let entityJSONIC = formatJSONIC(model).trim()
39
40
  entityJSONIC = entityJSONIC.substring(1, entityJSONIC.length - 1)
40
41
 
41
42
  const fieldAliasesSrc = fieldAliases(entity)
@@ -45,6 +46,7 @@ function resolveEntity(
45
46
  `main: ${KIT}: entity: ${entity.name}: {\n\n` +
46
47
  ` alias: field: ${fieldAliasesSrc}\n` +
47
48
  entityJSONIC +
49
+ relations +
48
50
  '\n\n}\n'
49
51
 
50
52
  entityFiles.push({ name: entityFile, src: entitySrc })
@@ -65,6 +67,20 @@ function resolveEntity(
65
67
  }
66
68
  }
67
69
 
70
+ function entityAncestorSource(entity: any): { model: any, relations: string } {
71
+ const model = { ...entity }
72
+ const ancestors: string[][] = entity.relations?.ancestors ?? []
73
+ if (null != entity.relations) {
74
+ model.relations = { ...entity.relations }
75
+ delete model.relations.ancestors
76
+ if (0 === Object.keys(model.relations).length) delete model.relations
77
+ }
78
+ const chains = ancestors.map(chain => ' [' + chain.map(name =>
79
+ 'path(' + JSON.stringify('$.main.kit.entity.' + name) + ')').join(' ') + ']')
80
+ return { model, relations: 0 === chains.length ? '' :
81
+ '\n relations: ancestors: [\n' + chains.join('\n') + '\n ]' }
82
+ }
83
+
68
84
 
69
85
  function gcEntityFiles(
70
86
  fs: any,
@@ -126,4 +142,5 @@ function fieldAliases(_entity: any): string {
126
142
  export {
127
143
  resolveEntity,
128
144
  gcEntityFiles,
145
+ entityAncestorSource,
129
146
  }
@@ -81,11 +81,11 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
81
81
 
82
82
  let num = (i * size(apiEntity.fields) * 10)
83
83
  each(apiEntity.fields, (field) => {
84
- ent[field.name] =
85
- 'number' === field.type ? num :
86
- 'boolean' === field.type ? 0 === num % 2 :
87
- 'object' === field.type ? {} :
88
- 'array' === field.type ? [] :
84
+ ent[field.n] =
85
+ 'number' === field.t ? num :
86
+ 'boolean' === field.t ? 0 === num % 2 :
87
+ 'object' === field.t ? {} :
88
+ 'array' === field.t ? [] :
89
89
  's' + (num.toString(16))
90
90
  num++
91
91
  })
@@ -108,9 +108,9 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
108
108
  const point = findMainLoadPoint(entop.load)
109
109
 
110
110
  // Get additional required match properties
111
- each(point?.args.params, (param: any) => {
112
- if (param.required) {
113
- let ancestorName = param.name
111
+ each(point?.g.params, (param: any) => {
112
+ if (param.r) {
113
+ let ancestorName = param.n
114
114
  let ancestorEntity = apimodel.main.api.entity[ancestorName]
115
115
 
116
116
  if (null == ancestorEntity) {
@@ -122,10 +122,10 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
122
122
  flow.model.param[`${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID`] = {
123
123
  [ancestorEntity.name + '01']: ancestorEntity.NAME + '01'
124
124
  }
125
- am[param.name] =
125
+ am[param.n] =
126
126
  `\`dm$=p.${model.NAME}_TEST_${ancestorEntity.NAME}_ENTID.${ancestorEntity.name}01\``
127
127
 
128
- data[`${nom(apiEntity, 'NAME')}01`][param.name] = ancestorEntity.NAME + '01'
128
+ data[`${nom(apiEntity, 'NAME')}01`][param.n] = ancestorEntity.NAME + '01'
129
129
  }
130
130
  }
131
131
  })
@@ -197,7 +197,7 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
197
197
 
198
198
 
199
199
  function findMainLoadPoint(op: ModelOp): ModelPoint | undefined {
200
- let cands = op.points.filter(a => 'id' === getelem(a.segments, -1)?.var)
200
+ let cands = op.points.filter(a => 'id' === getelem(a.s, -1)?.var)
201
201
  return cands[0]
202
202
  }
203
203
 
@@ -207,12 +207,12 @@ function makeUpdateData(name: string, apiEntity: any, flow: any, id: string) {
207
207
  const data = flow.model.test.entity[apiEntity.name]
208
208
 
209
209
  const dataFields =
210
- each(apiEntity.field).filter(f => 'id' !== f.name && !f.name.includes('_id'))
211
- const stringFields = each(dataFields).filter(f => 'string' === f.type)
210
+ each(apiEntity.fields).filter(f => 'id' !== f.n && !f.n.includes('_id'))
211
+ const stringFields = each(dataFields).filter(f => 'string' === f.t)
212
212
 
213
213
  if (0 < size(stringFields)) {
214
214
  const f = stringFields[0]
215
- ud[f.name] = data[id][f.name] + '-`$WHEN`'
215
+ ud[f.n] = data[id][f.n] + '-`$WHEN`'
216
216
  }
217
217
 
218
218
  return ud
package/src/cli.ts ADDED
@@ -0,0 +1,341 @@
1
+ /* Copyright (c) 2024-2026 Voxgig, MIT License */
2
+
3
+ // The command-line tool. `bin/voxgig-apidef` and the standalone executable
4
+ // entry points are shims over `main`, so the option handling and the project
5
+ // layout live in one place and under test.
6
+
7
+ import * as Fs from 'node:fs'
8
+ import Path from 'node:path'
9
+ import { parseArgs } from 'node:util'
10
+
11
+ import { Shape, Fault } from 'shape'
12
+
13
+ import { ApiDef } from './apidef'
14
+
15
+
16
+ const Pkg = require('../package.json')
17
+
18
+ const GUIDE_FILE = 'guide.aon'
19
+ const LEGACY_GUIDE_FILE = 'guide.aontu'
20
+ const BASE_GUIDE_FILE = 'base-guide.aon'
21
+
22
+ const WATCH_INTERVAL_MS = 500
23
+
24
+
25
+ type CliOptions = {
26
+ name: string
27
+ folder: string
28
+ def: string
29
+ prefix?: string
30
+ watch: boolean
31
+ debug?: string
32
+ help: boolean
33
+ version: boolean
34
+ extra?: string[]
35
+ }
36
+
37
+
38
+ type CliProject = {
39
+ root: string
40
+ folder: string
41
+ outprefix: string
42
+ def: string
43
+ model: { name: string, def: string }
44
+ guide: string
45
+ legacyguide: string
46
+ }
47
+
48
+
49
+ type CliIO = {
50
+ log: (...args: any[]) => void
51
+ error: (...args: any[]) => void
52
+ }
53
+
54
+
55
+ const CONSOLE_IO: CliIO = {
56
+ log: (...args: any[]) => console.log(...args),
57
+ error: (...args: any[]) => console.error(...args),
58
+ }
59
+
60
+
61
+ function usage(): string {
62
+ return [
63
+ 'Usage: voxgig-apidef <name> [options]',
64
+ '',
65
+ 'Build the API model for project <name> from an OpenAPI, Swagger or',
66
+ 'GraphQL definition file.',
67
+ '',
68
+ 'Options:',
69
+ ' -f, --folder <dir> project folder (default: <name>)',
70
+ ' -d, --def <file> the API definition file (required)',
71
+ ' -p, --prefix <text> prefix for generated file names (default: <name>-)',
72
+ ' -w, --watch rebuild when the definition file changes',
73
+ ' -g, --debug <level> log level (debug, info, warn, error); also writes',
74
+ ' the resolved definition as <def>.full.json',
75
+ ' -h, --help print this help and exit',
76
+ ' -v, --version print the package version and exit',
77
+ '',
78
+ 'The model is written to <folder>/model, which must already hold the guide',
79
+ 'entry file <folder>/model/guide/<prefix>' + GUIDE_FILE + ':',
80
+ '',
81
+ ' @"@voxgig/apidef/model/' + GUIDE_FILE + '"',
82
+ ' @"./<prefix>' + BASE_GUIDE_FILE + '"',
83
+ ].join('\n')
84
+ }
85
+
86
+
87
+ function resolveOptions(argv: string[]): CliOptions {
88
+ const args = parseArgs({
89
+ args: argv,
90
+ allowPositionals: true,
91
+ options: {
92
+ folder: { type: 'string', short: 'f', default: '' },
93
+ def: { type: 'string', short: 'd', default: '' },
94
+ prefix: { type: 'string', short: 'p' },
95
+ watch: { type: 'boolean', short: 'w' },
96
+ debug: { type: 'string', short: 'g' },
97
+ help: { type: 'boolean', short: 'h' },
98
+ version: { type: 'boolean', short: 'v' },
99
+ }
100
+ })
101
+
102
+ const [name, ...extra] = args.positionals
103
+
104
+ return {
105
+ name,
106
+ folder: '' === args.values.folder ? name : args.values.folder,
107
+ def: args.values.def,
108
+ prefix: args.values.prefix,
109
+ watch: !!args.values.watch,
110
+ debug: args.values.debug,
111
+ help: !!args.values.help,
112
+ version: !!args.values.version,
113
+ extra,
114
+ }
115
+ }
116
+
117
+
118
+ function validateOptions(rawOptions: CliOptions): CliOptions {
119
+ // An absent prefix defaults to <name>- later, an empty one is a valid
120
+ // choice, an absent debug leaves the library its own default, and `extra`
121
+ // is not an option at all; the shape rejects all four, so they are taken
122
+ // out and checked here. A positional after the name is a typo rather than
123
+ // a spare, and was being dropped without a word.
124
+ const { prefix, debug, extra, ...shaped } = rawOptions
125
+
126
+ if (null != extra && 0 < extra.length) {
127
+ throw new Error(
128
+ 'Unexpected extra arguments: ' + extra.join(' ') + '\n\n' + usage())
129
+ }
130
+
131
+ const optShape = Shape({
132
+ name: Fault('The first argument should be the project name.', String),
133
+ folder: String,
134
+ def: Fault('A definition file is required: --def <file>.', String),
135
+ watch: Boolean,
136
+ help: Boolean,
137
+ version: Boolean,
138
+ })
139
+
140
+ if (null != prefix && 'string' !== typeof prefix) {
141
+ throw new Error('The prefix should be a string.')
142
+ }
143
+ if (null != debug && 'string' !== typeof debug) {
144
+ throw new Error('The debug level should be a string.')
145
+ }
146
+
147
+ const err: any[] = []
148
+ const options: CliOptions = optShape(shaped, { err })
149
+
150
+ if (err[0]) {
151
+ throw new Error(err[0].text)
152
+ }
153
+
154
+ options.prefix = prefix
155
+ options.debug = debug
156
+
157
+ options.def = Path.resolve(options.def)
158
+ const stat = Fs.statSync(options.def, { throwIfNoEntry: false })
159
+ if (null == stat) {
160
+ throw new Error('Definition file not found: ' + options.def)
161
+ }
162
+
163
+ return options
164
+ }
165
+
166
+
167
+ // A name still absolute after Path.relative is on another drive, which the
168
+ // pipeline's <base>/../def join cannot reach.
169
+ function defName(deffolder: string, def: string, path: typeof Path = Path): string {
170
+ const name = path.relative(deffolder, def)
171
+
172
+ if (path.isAbsolute(name)) {
173
+ throw new Error(
174
+ 'Definition file must be on the same drive as the project folder: ' + def)
175
+ }
176
+
177
+ return name
178
+ }
179
+
180
+
181
+ // The pipeline reads the definition at <base>/../def/<model.def> and writes
182
+ // under the output folder; both are <root>/model here, so a definition kept
183
+ // anywhere is named relative to <root>/def.
184
+ function resolveProject(options: CliOptions): CliProject {
185
+ const root = Path.resolve(options.folder)
186
+ const folder = Path.join(root, 'model')
187
+ const outprefix = null == options.prefix ? options.name + '-' : options.prefix
188
+ const def = Path.resolve(options.def)
189
+ const guidefolder = Path.join(folder, 'guide')
190
+
191
+ return {
192
+ root,
193
+ folder,
194
+ outprefix,
195
+ def,
196
+ model: {
197
+ name: options.name,
198
+ def: defName(Path.join(folder, '..', 'def'), def),
199
+ },
200
+ guide: Path.join(guidefolder, outprefix + GUIDE_FILE),
201
+ legacyguide: Path.join(guidefolder, outprefix + LEGACY_GUIDE_FILE),
202
+ }
203
+ }
204
+
205
+
206
+ // A legacy `.aontu` guide is accepted here because the guide stage migrates
207
+ // it to `.aon` before reading it.
208
+ function checkProject(project: CliProject): void {
209
+ if (Fs.existsSync(project.guide) || Fs.existsSync(project.legacyguide)) {
210
+ return
211
+ }
212
+
213
+ throw new Error(
214
+ 'Guide entry file not found: ' + project.guide + '\n' +
215
+ 'Create it with these two lines:\n' +
216
+ ' @"@voxgig/apidef/model/' + GUIDE_FILE + '"\n' +
217
+ ' @"./' + project.outprefix + BASE_GUIDE_FILE + '"')
218
+ }
219
+
220
+
221
+ // The closure makeBuild returns memoises the ApiDef instance and its logger,
222
+ // so a watch that reuses it rebuilds the model without rebuilding those.
223
+ async function makeRunBuild(
224
+ project: CliProject, options: CliOptions): Promise<() => Promise<any>> {
225
+ const build = await ApiDef.makeBuild({
226
+ folder: project.folder,
227
+ outprefix: project.outprefix,
228
+ debug: options.debug,
229
+ })
230
+
231
+ return () => build(project.model, { spec: { base: project.folder } }, {})
232
+ }
233
+
234
+
235
+ function report(result: any, project: CliProject, io: CliIO): void {
236
+ if (result.ok) {
237
+ const entities = Object.keys(result.apimodel?.main?.kit?.entity || {})
238
+ io.log('voxgig-apidef: ok' +
239
+ ' model: ' + project.folder +
240
+ ' entities: ' + (0 < entities.length ? entities.join(' ') : 'none'))
241
+ }
242
+ else {
243
+ const last = result.steps?.[result.steps.length - 1] || 'start'
244
+ io.error('voxgig-apidef: failed after step ' + last + ': ' +
245
+ (result.err?.message || 'unknown error'))
246
+ }
247
+ }
248
+
249
+
250
+ function watchDef(project: CliProject, rebuild: () => Promise<void>, io: CliIO): Promise<never> {
251
+ return new Promise(() => {
252
+ let running = false
253
+ let pending = false
254
+
255
+ const run = async () => {
256
+ if (running) {
257
+ pending = true
258
+ return
259
+ }
260
+ running = true
261
+ try {
262
+ await rebuild()
263
+ }
264
+ finally {
265
+ running = false
266
+ if (pending) {
267
+ pending = false
268
+ await run()
269
+ }
270
+ }
271
+ }
272
+
273
+ Fs.watchFile(project.def, { interval: WATCH_INTERVAL_MS }, () => { run() })
274
+ io.log('voxgig-apidef: watching ' + project.def)
275
+ })
276
+ }
277
+
278
+
279
+ async function runCli(argv: string[], io: CliIO = CONSOLE_IO): Promise<number> {
280
+ try {
281
+ let options = resolveOptions(argv)
282
+
283
+ if (options.version) {
284
+ io.log(Pkg.version)
285
+ return 0
286
+ }
287
+
288
+ if (options.help) {
289
+ io.log(usage())
290
+ return 0
291
+ }
292
+
293
+ options = validateOptions(options)
294
+
295
+ const project = resolveProject(options)
296
+ checkProject(project)
297
+
298
+ const runBuild = await makeRunBuild(project, options)
299
+
300
+ const result = await runBuild()
301
+ report(result, project, io)
302
+
303
+ if (options.watch) {
304
+ await watchDef(project, async () => {
305
+ report(await runBuild(), project, io)
306
+ }, io)
307
+ }
308
+
309
+ return result.ok ? 0 : 1
310
+ }
311
+ catch (err: any) {
312
+ io.error('Voxgig API Definition Error:')
313
+ io.error(err?.message || err)
314
+ return 1
315
+ }
316
+ }
317
+
318
+
319
+ function main(): void {
320
+ runCli(process.argv.slice(2)).then((code) => {
321
+ process.exitCode = code
322
+ })
323
+ }
324
+
325
+
326
+ export {
327
+ main,
328
+ runCli,
329
+ resolveOptions,
330
+ validateOptions,
331
+ defName,
332
+ resolveProject,
333
+ checkProject,
334
+ usage,
335
+ }
336
+
337
+ export type {
338
+ CliOptions,
339
+ CliProject,
340
+ CliIO,
341
+ }
@@ -54,12 +54,16 @@ function migrateLegacyGuide(fs: any, folder: string, guideprefix: string): boole
54
54
  return false
55
55
  }
56
56
 
57
- const legacysrc = String(fs.readFileSync(legacyguide, 'utf8'))
58
- const migrated = legacysrc
57
+ let migrated = String(fs.readFileSync(legacyguide, 'utf8'))
59
58
  .replace(/@"@voxgig\/apidef\/model\/guide\.aontu"/g,
60
59
  '@"@voxgig/apidef/model/guide.aon"')
61
- .split('@"' + guideprefix + 'base-guide.aontu"')
62
- .join('@"' + guideprefix + 'base-guide.aon"')
60
+
61
+ // The sibling include is written bare or with `./`; both name this file.
62
+ for (const dir of ['', './']) {
63
+ migrated = migrated
64
+ .split('@"' + dir + guideprefix + 'base-guide.aontu"')
65
+ .join('@"' + dir + guideprefix + 'base-guide.aon"')
66
+ }
63
67
 
64
68
  fs.writeFileSync(guidepath, migrated)
65
69
  try { fs.unlinkSync(legacyguide) } catch (_err: any) { }
package/src/model.ts CHANGED
@@ -61,17 +61,18 @@ type ModelFieldOp = {
61
61
 
62
62
 
63
63
  type ModelField = {
64
- name: string
65
- type: any
66
- req: boolean
64
+ n: string
65
+ h: string
66
+ t: any
67
+ r: boolean
67
68
  op: Partial<Record<OpName, ModelFieldOp>>
68
69
 
69
- short?: string
70
+ sh?: string
70
71
 
71
- readOnly?: boolean
72
- writeOnly?: boolean
73
- deprecated?: boolean
74
- format?: string
72
+ ro?: boolean
73
+ wo?: boolean
74
+ de?: boolean
75
+ fo?: string
75
76
  union?: {
76
77
  count: number
77
78
  branches: number
@@ -80,18 +81,14 @@ type ModelField = {
80
81
  }
81
82
 
82
83
 
83
- // Operation argument/parameter definition.
84
- // `example` captures a value the spec advertises (parameter `example`,
85
- // the first entry of `examples`, or `schema.example`/`schema.default`).
86
- // Test generators use this for required params in live test setup so the
87
- // generated request actually satisfies the API contract.
84
+ // `ex` holds an advertised parameter example or schema default.
88
85
  type ModelArg = {
89
- name: string
90
- orig: string
91
- type: any
92
- kind: ArgKind
93
- reqd: boolean
94
- example?: any
86
+ n: string
87
+ or?: string
88
+ t: any
89
+ k: ArgKind
90
+ r: boolean
91
+ ex?: any
95
92
  }
96
93
 
97
94
 
@@ -133,29 +130,30 @@ type ModelPathSegment = {
133
130
 
134
131
 
135
132
  type ModelPoint = {
136
- contract?: { version: number, id: string, source: string, json: string }
137
- orig: string
138
- kind?: PointKind
139
- graphql?: ModelGraphql
140
- method: MethodName
141
- segments: ModelPathSegment[]
142
- rename: Partial<{
133
+ co?: { version: 2, id: string, source: string }
134
+ li?: boolean | Record<string, any>
135
+ o: string
136
+ k?: PointKind
137
+ gq?: ModelGraphql
138
+ m: MethodName
139
+ s: ModelPathSegment[]
140
+ r: Partial<{
143
141
  param: Record<string, string>
144
142
  query: Record<string, string>
145
143
  header: Record<string, string>
146
144
  cookie: Record<string, string>
147
145
  }>
148
- args: Partial<{
146
+ g: Partial<{
149
147
  params: ModelArg[]
150
148
  query: ModelArg[]
151
149
  header: ModelArg[]
152
150
  cookie: ModelArg[]
153
151
  }>
154
- transform: {
152
+ t: {
155
153
  req?: any
156
154
  res?: any
157
155
  }
158
- select: {
156
+ q: {
159
157
  exist: string[]
160
158
  $action?: string
161
159
  }
@@ -173,7 +171,7 @@ type ModelEntity = {
173
171
  Name?: string
174
172
  NAME?: string
175
173
  op: ModelOpMap
176
- fields: ModelField[]
174
+ fields: Record<string, ModelField>
177
175
  id?: {
178
176
  name: string
179
177
  field: string
@@ -197,7 +195,7 @@ type ModelEntityFlow = {
197
195
  // Per-step input cluster. Test-generators name the variables they emit by
198
196
  // reading these slots, falling back to derived defaults. All fields are
199
197
  // optional — `newFlowStep` in transform/flowstep.ts guarantees the input
200
- // object itself exists, so consumers don't need to null-check `step.input`.
198
+ // object itself exists, so consumers don't need to null-check `step.i`.
201
199
  type ModelEntityFlowStepInput = {
202
200
  ref?: string
203
201
  entvar?: string
@@ -226,12 +224,13 @@ type ModelEntityFlowStepSpec = {
226
224
 
227
225
 
228
226
  type ModelEntityFlowStep = {
229
- op: OpName
230
- input: ModelEntityFlowStepInput
231
- match: Record<string, any>
232
- data: Record<string, any>
233
- spec: ModelEntityFlowStepSpec[]
234
- valid: ModelEntityFlowStepValidator[]
227
+ a?: boolean
228
+ o: OpName
229
+ i: ModelEntityFlowStepInput
230
+ m: Record<string, any>
231
+ d: Record<string, any>
232
+ s: ModelEntityFlowStepSpec[]
233
+ v: ModelEntityFlowStepValidator[]
235
234
  }
236
235
 
237
236