@voxgig/apidef 8.11.0 → 8.12.1

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 (79) hide show
  1. package/dist/apidef.d.ts +4 -1
  2. package/dist/apidef.js +12 -34
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.js +0 -33
  5. package/dist/builder/entity/entity.js.map +1 -1
  6. package/dist/builder/entity/info.js +0 -4
  7. package/dist/builder/entity/info.js.map +1 -1
  8. package/dist/builder/flow.d.ts +4 -1
  9. package/dist/builder/flow.js +36 -27
  10. package/dist/builder/flow.js.map +1 -1
  11. package/dist/guide/graphql01.js +0 -55
  12. package/dist/guide/graphql01.js.map +1 -1
  13. package/dist/guide/guide.js +3 -126
  14. package/dist/guide/guide.js.map +1 -1
  15. package/dist/guide/heuristic01.js +40 -181
  16. package/dist/guide/heuristic01.js.map +1 -1
  17. package/dist/parse/graphql.js +0 -25
  18. package/dist/parse/graphql.js.map +1 -1
  19. package/dist/parse.js +51 -56
  20. package/dist/parse.js.map +1 -1
  21. package/dist/resolved.d.ts +23 -0
  22. package/dist/resolved.js +92 -0
  23. package/dist/resolved.js.map +1 -0
  24. package/dist/resolver.js +0 -2
  25. package/dist/resolver.js.map +1 -1
  26. package/dist/transform/args.js +2 -25
  27. package/dist/transform/args.js.map +1 -1
  28. package/dist/transform/casecollide.d.ts +3 -0
  29. package/dist/transform/casecollide.js +67 -0
  30. package/dist/transform/casecollide.js.map +1 -0
  31. package/dist/transform/contract.js +43 -38
  32. package/dist/transform/contract.js.map +1 -1
  33. package/dist/transform/entity.js +0 -56
  34. package/dist/transform/entity.js.map +1 -1
  35. package/dist/transform/field.js +1 -368
  36. package/dist/transform/field.js.map +1 -1
  37. package/dist/transform/flowstep.js +0 -49
  38. package/dist/transform/flowstep.js.map +1 -1
  39. package/dist/transform/graphql.js +0 -36
  40. package/dist/transform/graphql.js.map +1 -1
  41. package/dist/transform/operation.js +0 -23
  42. package/dist/transform/operation.js.map +1 -1
  43. package/dist/transform/top.js +1 -128
  44. package/dist/transform/top.js.map +1 -1
  45. package/dist/transform.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +2 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/utility.d.ts +2 -1
  50. package/dist/utility.js +17 -316
  51. package/dist/utility.js.map +1 -1
  52. package/model/apidef.aon +20 -113
  53. package/model/guide.aon +1 -32
  54. package/package.json +5 -5
  55. package/src/apidef.ts +22 -33
  56. package/src/builder/entity/entity.ts +0 -33
  57. package/src/builder/entity/info.ts +0 -4
  58. package/src/builder/flow.ts +39 -24
  59. package/src/desc.ts +0 -22
  60. package/src/guide/graphql01.ts +0 -75
  61. package/src/guide/guide.ts +5 -127
  62. package/src/guide/heuristic01.ts +47 -182
  63. package/src/model.ts +6 -85
  64. package/src/parse/graphql.ts +0 -25
  65. package/src/parse.ts +61 -56
  66. package/src/resolved.ts +136 -0
  67. package/src/resolver.ts +0 -2
  68. package/src/transform/args.ts +2 -25
  69. package/src/transform/casecollide.ts +80 -0
  70. package/src/transform/contract.ts +44 -34
  71. package/src/transform/entity.ts +0 -56
  72. package/src/transform/field.ts +2 -368
  73. package/src/transform/flowstep.ts +0 -49
  74. package/src/transform/graphql.ts +0 -36
  75. package/src/transform/operation.ts +0 -23
  76. package/src/transform/top.ts +1 -131
  77. package/src/transform.ts +0 -1
  78. package/src/types.ts +4 -13
  79. package/src/utility.ts +18 -314
@@ -47,6 +47,7 @@ import {
47
47
  capture,
48
48
  cleanComponentName,
49
49
  debugpath,
50
+ debugpathOn,
50
51
  depluralize,
51
52
  ensureMinEntityName,
52
53
  find,
@@ -112,7 +113,6 @@ const METHOD_CONSIDER_ORDER: Record<string, number> = {
112
113
 
113
114
  async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
114
115
 
115
- // TODO: Ordu needs better debug output to track task exec
116
116
  const analysis = new Ordu({ select: { sort: true } }).add([
117
117
  Prepare,
118
118
  {
@@ -145,12 +145,6 @@ async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
145
145
 
146
146
  const guide = result.data.guide
147
147
 
148
- // Reassign single-segment collection paths (e.g. "/people") onto the entity
149
- // that owns the per-instance path ("/people/{id}"). Heuristic discovery can
150
- // split the two when response schemas wrap the resource in a search/pagination
151
- // component. Running this here — before base-guide.aontu is serialised —
152
- // means the same merged layout flows into both guide-case (transformers off)
153
- // and model-case (transformers on), so they agree on the final guide.
154
148
  mergeCollectionPaths(guide, ctx.log)
155
149
 
156
150
  const metrics = guide.metrics
@@ -227,7 +221,6 @@ function Prepare(spec: TaskSpec) {
227
221
  function MeasurePath(spec: TaskSpec) {
228
222
  const guide = spec.data.guide
229
223
  const metrics = guide.metrics
230
- // const pathstr = spec.node.key
231
224
  const pathdef = spec.node.val
232
225
 
233
226
  metrics.count.path++
@@ -250,7 +243,6 @@ function MeasurePath(spec: TaskSpec) {
250
243
  function MeasureMethod(spec: TaskSpec) {
251
244
  const guide = spec.data.guide
252
245
  const metrics = guide.metrics
253
- // const methodstr = spec.node.key
254
246
  const methoddef = spec.node.val
255
247
 
256
248
  const pathtags = methoddef.tags
@@ -316,31 +308,7 @@ function MeasureRef(spec: TaskSpec) {
316
308
 
317
309
  function selectAllMethods(_source: any, spec: TaskSpec): MethodDesc[] {
318
310
  const ctx = spec.ctx
319
- // const paths = ctx.def.paths
320
-
321
- /*
322
- let caught = capture(ctx.def, {
323
- paths:
324
- ['`$SELECT`', /.* /,
325
- ['`$SELECT`', /^get|post|put|patch|delete$/i,
326
- ['`$APPEND`', 'methods', {
327
- path: '`select$=key.paths`',
328
- method: { '`$UPPER`': '`$KEY`' },
329
- summary: '`.summary`',
330
- tags: '`.tags`',
331
- parameters: '`.parameters`',
332
- responses: '`.responses`',
333
- requestBody: '`.requestBody`'
334
- }]
335
- ]
336
- ]
337
- })
338
-
339
- // TODO: capture should return these empty objects
340
- caught = caught ?? {}
341
- caught.methods = caught.methods ?? []
342
311
 
343
- */
344
312
 
345
313
  let caught: any = { methods: [] }
346
314
 
@@ -424,7 +392,6 @@ function ResolveEntityComponent(spec: TaskSpec) {
424
392
  })
425
393
  .filter(xref => null != xref.cmp)
426
394
 
427
- // TODO: identify non - ent schemas
428
395
  .filter(xref => !xref.val.includes('Meta'))
429
396
 
430
397
  let cleanxrefs = cmpxrefs
@@ -607,19 +574,6 @@ function ResolveEntityName(spec: TaskSpec) {
607
574
  }
608
575
  }
609
576
 
610
- // A PATH SEGMENT CARRIES NO WORD BOUNDARIES, and the component name does.
611
- //
612
- // `/openbanking/payeeverification` is one lowercase run, so `canonize` has
613
- // nothing to split on and the entity is named `payeeverification` ->
614
- // `Payeeverification`. The boundary was never missing: the response
615
- // component for that same operation is
616
- // `PayeeVerification.PayeeVerificationResult`, which canonizes to
617
- // `payee_verification_result`.
618
- //
619
- // Where the path wins over the component -- which is usually RIGHT, because
620
- // a widely-reused response schema makes a poor entity name -- the component
621
- // was being discarded whole, boundaries and all. Borrow just the boundaries
622
- // back. See resplitFromCmp for why this cannot invent a name.
623
577
  entname = resplitFromCmp(entname, ment.cmp as string, why_path)
624
578
 
625
579
  // Keep the pre-truncation name so a truncated-name collision can tell a
@@ -658,8 +612,11 @@ function ResolveEntityName(spec: TaskSpec) {
658
612
  work.pathowner[pathStr] = work.pathowner[pathStr] ?? {}
659
613
  work.pathowner[pathStr][methodName] = entname
660
614
 
661
- debugpath(pathStr, methodName, 'RESOLVE-ENTITY-NAME',
662
- formatJSONIC({ entdesc, ment }, { hsepd: 0, $: true, color: true }))
615
+ // Same guard, same reason: the formatting is the cost, not the call.
616
+ if (debugpathOn()) {
617
+ debugpath(pathStr, methodName, 'RESOLVE-ENTITY-NAME',
618
+ formatJSONIC({ entdesc, ment }, { hsepd: 0, $: true, color: true }))
619
+ }
663
620
  }
664
621
 
665
622
 
@@ -683,12 +640,6 @@ function RenameParams(spec: TaskSpec) {
683
640
 
684
641
  const methodName = mdesc.method
685
642
 
686
- // Rewrite path parameters that are identifiers to follow the rules:
687
- // 0. Parameters named [a-z]?id are considered identifiers
688
- // 1. last identifier is always {id} as this is the primary entity
689
- // 2. internal identifiers are formatted as {name_id} where name is the parent entity name
690
- // Example: /api/bar/{id}/zed/{zid}/foo/{fid} ->
691
- // /api/bar/{bar_id}/zed/{zed_id}/foo/{id}
692
643
 
693
644
  const pathDesc = entdesc.path[pathStr]
694
645
  pathDesc.rename = (pathDesc.rename ?? { param: {} })
@@ -703,12 +654,6 @@ function RenameParams(spec: TaskSpec) {
703
654
  }
704
655
  const parts = pathdesc.parts
705
656
 
706
- // Implicit snake_case normalization for any path placeholder not already
707
- // renamed by the id-rename logic. apidef's args transform snake-cases param
708
- // names (e.g. spec `platformKey` → param.name `platform_key`); without
709
- // normalizing the placeholder to match, runtime URL substitution by
710
- // param.name fails to fill `{platformKey}`. Defined as a closure so we can
711
- // run it after the id-rename loop OR after the multi-param early-return.
712
657
  const applySnakeCaseRename = () => {
713
658
  for (const part of parts) {
714
659
  const m = part.match(/^\{(.+)\}$/)
@@ -798,7 +743,6 @@ function RenameParams(spec: TaskSpec) {
798
743
  || parentName === cmpname
799
744
  )
800
745
  ) {
801
- // let newParamName = 'id'
802
746
  updateParamRename(
803
747
  ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
804
748
  'id', 'action-parent:' + entdesc.name)
@@ -897,8 +841,6 @@ function RenameParams(spec: TaskSpec) {
897
841
  'id', 'id-not-last')
898
842
 
899
843
  why.push('id-not-last')
900
- // paramRenames[oldParam] = 'id'
901
- // paramRenamesWhy[oldParam].push('id-not-last')
902
844
  }
903
845
 
904
846
  // Not primary ent.
@@ -912,8 +854,6 @@ function RenameParams(spec: TaskSpec) {
912
854
  newParamName, 'not-primary')
913
855
  why.push('not-primary')
914
856
 
915
- // paramRenames[oldParam] = newParamName
916
- // paramRenamesWhy[oldParam].push('not-primary')
917
857
  }
918
858
  }
919
859
  }
@@ -926,7 +866,6 @@ function RenameParams(spec: TaskSpec) {
926
866
  delete paramRenameCapture.why[oldParam]
927
867
  }
928
868
 
929
- // TODO: these need to done via an API
930
869
  debugpath(pathStr, methodName, 'RENAME-PARAM',
931
870
  {
932
871
  pathStr,
@@ -968,7 +907,6 @@ function FindActions(spec: TaskSpec) {
968
907
  const entname = ment.entname
969
908
  const entdesc = work.entmap[entname]
970
909
 
971
- // const pathdesc = spec.data.work.pathmap[pathStr]
972
910
  const pathdesc = entdesc.path[pathStr]
973
911
 
974
912
  const methodName = mdesc.method
@@ -989,12 +927,6 @@ function FindActions(spec: TaskSpec) {
989
927
 
990
928
  const cmp = ment.cmp
991
929
 
992
- // A verb that ResolveEntityName assigned to its parent entity
993
- // (verbOnParent) is an action whatever the parent literal canonizes to:
994
- // `/app/installations/{installation_id}/access_tokens` belongs to `app`.
995
- // Recorded directly rather than through updateAction, whose guard against
996
- // an entity "already encoding" the verb would drop `archive` on
997
- // `email_archive` and leave the verb as a plain CRUD point.
998
930
  if (null != ment.verb_on_parent) {
999
931
  pathdesc.action[lastPartCanon] = pathdesc.action[lastPartCanon] ?? {
1000
932
  why_action: ['ent', entdesc.name, 'verb-on-parent', lastPart, methodName],
@@ -1068,15 +1000,7 @@ function ResolveOperation(spec: TaskSpec) {
1068
1000
  return
1069
1001
  }
1070
1002
 
1071
- // REVIEW: using POST and PUT in non-restian ways is too wierd to handle consistently
1072
- // correct using guide customizations
1073
1003
 
1074
- // Sometimes POST is used to update, not create. Attempt to identify this.
1075
- // And sometimes vice versa for PUT
1076
- // const id_param_offset = ment.pm?.expr?.endsWith('/t/') ? 1 : 0
1077
- // const has_end_id_param =
1078
- // entname == canonize(parts[parts.length - 2 - id_param_offset])
1079
- // && parts[parts.length - 1 - id_param_offset]?.toLowerCase().endsWith('id}')
1080
1004
 
1081
1005
 
1082
1006
  if ('load' === standard_opname) {
@@ -1084,30 +1008,12 @@ function ResolveOperation(spec: TaskSpec) {
1084
1008
  opname = islist ? 'list' : opname
1085
1009
  }
1086
1010
 
1087
- /*
1088
- else if (
1089
- 'create' === standard_opname
1090
- && has_end_id_param
1091
- ) {
1092
- opname = 'update'
1093
- why_op.push('id-present')
1094
- }
1095
-
1096
- else if (
1097
- 'update' === standard_opname
1098
- && !has_end_id_param
1099
- ) {
1100
- opname = 'create'
1101
- why_op.push('no-id-present')
1102
- }
1103
- */
1104
1011
 
1105
1012
 
1106
1013
  else {
1107
1014
  why_op.push('not-load')
1108
1015
  }
1109
1016
 
1110
- // why.push('ent=' + entdesc.name)
1111
1017
 
1112
1018
  ment.opname = opname
1113
1019
  ment.why_opname = why_op
@@ -1154,7 +1060,6 @@ function ResolveTransform(spec: TaskSpec) {
1154
1060
  const entname = mdesc.MethodEntity.entname
1155
1061
  const entdesc = work.entmap[entname]
1156
1062
 
1157
- // const pathdesc = spec.data.work.pathmap[pathStr]
1158
1063
  const pathdesc = entdesc.path[pathStr]
1159
1064
 
1160
1065
  const methodName = mdesc.method
@@ -1174,12 +1079,6 @@ function ResolveTransform(spec: TaskSpec) {
1174
1079
  debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
1175
1080
 
1176
1081
  if (resprops) {
1177
- // Only unwrap `body.<entity>` when the entity-named response property is
1178
- // itself a structured value (object/array/ref/composed schema) that could
1179
- // actually contain the entity. A scalar property that merely shares the
1180
- // entity's name (e.g. an entity `advice` whose own fields include a
1181
- // string field `advice`) is a FIELD of the entity, not a wrapper around
1182
- // it: the response IS the entity, so it must stay `body` (the default).
1183
1082
  if (isEntityWrapperProp(resprops[entdesc.origname])) {
1184
1083
  transform.res = '`body.' + entdesc.origname + '`'
1185
1084
  }
@@ -1199,14 +1098,6 @@ function ResolveTransform(spec: TaskSpec) {
1199
1098
  }
1200
1099
  }
1201
1100
 
1202
- // The SCHEMA is what closedBodyTransform needs (it reads
1203
- // additionalProperties); the wrapper-name checks need its PROPERTIES. They
1204
- // used to share one value and index the schema itself, so
1205
- // `schema['todoitem']` was always undefined and the entity-name request
1206
- // envelope was never detected — while the Go port read `.properties` and
1207
- // did detect it. That divergence was inert only because `req` was never
1208
- // serialised; now that it is, the two implementations would emit different
1209
- // request bodies for the same spec.
1210
1101
  const reqschema = getRequestBodySchema(mdesc.requestBody)
1211
1102
  const reqprops = reqschema?.properties
1212
1103
  debugpath(pathStr, methodName, 'TRANSFORM-REQ', keysof(reqprops))
@@ -1276,12 +1167,6 @@ function BuildEntity(spec: TaskSpec) {
1276
1167
  path,
1277
1168
  }
1278
1169
 
1279
- // An entity built ENTIRELY out of access-token exchanges is not a resource
1280
- // — it is the credential plumbing an SDK's auth layer performs. Emitted
1281
- // with `active: false` rather than dropped, so it stays visible in
1282
- // guide.aon and the classification can be reversed there (ADR-002): flip
1283
- // it to `true` and the entity comes back. The exchange itself is recorded
1284
- // separately as model facts by transform/top.ts.
1285
1170
  if (0 < entdesc.authexchange_ops && entdesc.authexchange_ops === entdesc.total_ops) {
1286
1171
  guideEntity.active = false
1287
1172
  guideEntity.why_inactive = 'auth-exchange'
@@ -1361,28 +1246,6 @@ function endsWithCmp(data: any, pm: PathMatch) {
1361
1246
  }
1362
1247
 
1363
1248
 
1364
- // A write on `.../<parent>/{id}/<verb>` is a VERB ON THE PARENT, not an
1365
- // entity named after its result shape.
1366
- //
1367
- // GitHub's `PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge` answers with
1368
- // a `pull-request-merge-result` component. Naming the method's entity after
1369
- // that component (the cmp-primary rule) produced a `pull_request_merge_result`
1370
- // entity with a single `update` op, while the GET on the same path (no
1371
- // response schema) stayed an action on `pull`: one route split across two
1372
- // entities by method, and the verb unreachable from the entity it acts on.
1373
- //
1374
- // Five signals, together: the method writes (a GET on such a path is a
1375
- // sub-resource read and keeps the component rule); the response component
1376
- // occurs nowhere else in the spec (a one-off result, not a resource shape);
1377
- // that component is not the literal's own collection shape (a create-only
1378
- // `POST .../{id}/labels` answering with a `label` is a nested collection,
1379
- // not a verb); the item selector itself (`.../pulls/{pull_number}`) is a
1380
- // path of the spec, so the trailing literal cannot be a collection of its
1381
- // own; and nothing extends the path (`.../private-registries/{secret_name}`
1382
- // makes `private-registries` a collection, whatever its POST answers with).
1383
- // The verb then joins the parent entity, where FindActions records it as an
1384
- // action and select stamps `$action` on its points. Returns the parent's
1385
- // entity name, or null when the rule does not apply.
1386
1249
  function verbOnParent(
1387
1250
  data: { def: any, guide: any, work: any },
1388
1251
  pm: PathMatch,
@@ -1398,21 +1261,11 @@ function verbOnParent(
1398
1261
  return null
1399
1262
  }
1400
1263
 
1401
- // A PLURAL literal names a nested collection, whatever it answers with:
1402
- // `asset_keys` under `{environment_id}` creates an asset key, and
1403
- // `approvals` under `{merge_request_iid}` is a collection of approvals.
1404
- // A verb is singular — `merge`, `revoke`, `resend_confirmation` — so the
1405
- // component rule below is never reached for a plural, which is what keeps
1406
- // a create-only collection an entity of its own.
1407
1264
  const lit = snakify(getelem(pm, -1))
1408
1265
  if ('' === lit || depluralize(lit) !== lit) {
1409
1266
  return null
1410
1267
  }
1411
1268
 
1412
- // A singular literal still names a collection when its response component
1413
- // is that collection's member shape (`label` answering with `label`, or
1414
- // with a parent-prefixed `thing_label`); a verb answers with something
1415
- // else.
1416
1269
  const verb = canonize(getelem(pm, -1))
1417
1270
  const cmp = String(ment.cmp ?? '')
1418
1271
  if ('' === verb || cmp === verb || cmp.endsWith('_' + verb)) {
@@ -1477,7 +1330,6 @@ function entityPathMatch_tpe(
1477
1330
 
1478
1331
  why.push('path=t/p/')
1479
1332
  const origPathName = pm[pathNameIndex]
1480
- // let entname = fixEntName(origPathName)
1481
1333
  let entname = canonize(origPathName)
1482
1334
 
1483
1335
  if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
@@ -1524,7 +1376,6 @@ function entityPathMatch_te(
1524
1376
 
1525
1377
  why.push('path=t/')
1526
1378
  const origPathName = pm[pathNameIndex]
1527
- // let entname = fixEntName(origPathName)
1528
1379
  let entname = canonize(origPathName)
1529
1380
 
1530
1381
  if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
@@ -1548,7 +1399,6 @@ function entityPathMatch_tpp(
1548
1399
 
1549
1400
  why.push('path=t/p/p')
1550
1401
  const origPathName = pm[pathNameIndex]
1551
- // let entname = fixEntName(origPathName)
1552
1402
  let entname = canonize(origPathName)
1553
1403
 
1554
1404
  if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
@@ -1615,7 +1465,6 @@ function inferEntityName(
1615
1465
  }
1616
1466
 
1617
1467
 
1618
- // No entity component was found, but there still might be an entity.
1619
1468
  function probableEntityMethod(
1620
1469
  data: { def: any },
1621
1470
  mdesc: any,
@@ -1796,14 +1645,6 @@ function isListResponse(
1796
1645
  let islist = false
1797
1646
  let schema
1798
1647
 
1799
- // 'p/' is anchored (e.g. t/p/): the path ends at a param, so it is an
1800
- // item path and the response shape cannot change that.
1801
- //
1802
- // A bare trailing 'p' (e.g. t/p/p, a compound key like
1803
- // /repos/{owner}/{repo}) also ends at a param, but the same shape covers
1804
- // a sub-collection scoped by a compound key (e.g. /audit-log/{ns}/{repo}).
1805
- // Those are told apart by the response: a collection returns an array at
1806
- // the top level, an item does not.
1807
1648
  const endParamAnchored = !!(pm && pm.expr.endsWith('p/'))
1808
1649
  const endParamBare = !!(pm && !endParamAnchored && pm.expr.endsWith('p'))
1809
1650
 
@@ -1823,11 +1664,6 @@ function isListResponse(
1823
1664
  islist = true
1824
1665
  }
1825
1666
 
1826
- // The array-prop fallback is deliberately loose, and an item schema
1827
- // often carries an incidental array property (GitHub's full-repository
1828
- // has topics: string[]). That is good enough evidence for an ordinary
1829
- // path, but not for a compound-key path, where it is exactly what
1830
- // misclassifies the item as a list.
1831
1667
  if (!islist && !endParamBare) {
1832
1668
  const properties = resolveSchemaProperties(schema)
1833
1669
 
@@ -1911,6 +1747,47 @@ function updateParamRename(
1911
1747
  newParamName: string,
1912
1748
  why: string,
1913
1749
  ) {
1750
+ // A name that cannot be an identifier is not an improvement on the one the
1751
+ // specification gave. See docs/design/derived-names.md
1752
+ if (!/^[A-Za-z_]/.test(newParamName)) {
1753
+ ctx.log.debug({
1754
+ point: 'param-rename-skip',
1755
+ path,
1756
+ param: oldParamName,
1757
+ rejected: newParamName,
1758
+ note: 'derived parameter name is not an identifier, keeping the' +
1759
+ " specification's name"
1760
+ })
1761
+ return
1762
+ }
1763
+
1764
+ // The clash is with what the path's OTHER parameters end up called: their
1765
+ // rename if they have one, their canonical name if not.
1766
+ const otherParams = (path.match(/\{([^}]+)\}/g) || [])
1767
+ .map((seg) => seg.slice(1, -1))
1768
+ .filter((name) => name !== oldParamName)
1769
+
1770
+ const takenBy =
1771
+ Object.keys(paramRenameCapture.rename)
1772
+ .find((other) => other !== oldParamName &&
1773
+ paramRenameCapture.rename[other] === newParamName) ??
1774
+ otherParams
1775
+ .find((other) => null == paramRenameCapture.rename[other] &&
1776
+ canonize(other) === newParamName)
1777
+
1778
+ if (null != takenBy) {
1779
+ ctx.log.debug({
1780
+ point: 'param-rename-collision',
1781
+ path,
1782
+ param: oldParamName,
1783
+ rejected: newParamName,
1784
+ takenBy,
1785
+ note: 'another parameter of this path already renames to ' +
1786
+ newParamName + ", keeping the specification's name"
1787
+ })
1788
+ return
1789
+ }
1790
+
1914
1791
  const existingNewName = paramRenameCapture.rename[oldParamName]
1915
1792
  const existingWhy = paramRenameCapture.why[oldParamName]
1916
1793
 
@@ -1923,9 +1800,6 @@ function updateParamRename(
1923
1800
  }
1924
1801
  }
1925
1802
  else if (newParamName == existingNewName) {
1926
- // if (!existingWhy.includes(why)) {
1927
- // existingWhy.push(why)
1928
- // }
1929
1803
  }
1930
1804
  else {
1931
1805
  ctx.warn({
@@ -1945,14 +1819,6 @@ function isParam(partStr: string) {
1945
1819
  }
1946
1820
 
1947
1821
 
1948
- /*
1949
- function fixEntName(origName: string) {
1950
- if (null == origName) {
1951
- return origName
1952
- }
1953
- return depluralize(snakify(origName))
1954
- }
1955
- */
1956
1822
 
1957
1823
 
1958
1824
  function findcmps(
@@ -1964,7 +1830,6 @@ function findcmps(
1964
1830
  const cmplist: string[] = []
1965
1831
  const cmpset = new Set<string>()
1966
1832
 
1967
- // TODO: cache in ctx.work
1968
1833
 
1969
1834
  each(data.def.paths[pathStr])
1970
1835
  .map((md: MethodDef) => {
package/src/model.ts CHANGED
@@ -1,15 +1,12 @@
1
1
  /* Copyright (c) 2024-2025 Voxgig, MIT License */
2
2
 
3
- // Consolidated model types for the API model derived from OpenAPI specifications
4
3
 
5
4
  import type { MethodName } from './types'
6
5
 
7
6
 
8
- // Operation names available on entities
9
7
  type OpName = 'load' | 'list' | 'create' | 'update' | 'remove' | 'patch' | 'head' | 'options'
10
8
 
11
9
 
12
- // Argument kinds supported on operation points.
13
10
  type ArgKind = 'param' | 'query' | 'header' | 'cookie'
14
11
 
15
12
 
@@ -49,62 +46,36 @@ type Model = NamesCluster & {
49
46
  }
50
47
 
51
48
 
52
- // Entity relationships information
53
49
  type ModelEntityRelations = {
54
50
  ancestors: string[][]
55
51
  }
56
52
 
57
53
 
58
- // Map of operations available on an entity
59
54
  type ModelOpMap = Partial<Record<OpName, ModelOp | undefined>>
60
55
 
61
56
 
62
- // Field-specific operation configuration
63
57
  type ModelFieldOp = {
64
- type: any // @voxgig/struct validation schema
58
+ type: any
65
59
  req: boolean
66
60
  }
67
61
 
68
62
 
69
- // Entity field definition
70
- //
71
- // `union` is present only when the field bottoms out in an UNTAGGED union —
72
- // `oneOf`/`anyOf`, two or more branches, no `discriminator` — so the spec
73
- // never says which variant a value is and the field can only be modelled as
74
- // an open type. It records the widest such union found beneath the field, and
75
- // exists so generators can SAY SO in the documentation rather than silently
76
- // emitting a permissive type that looks like a modelling failure.
77
63
  type ModelField = {
78
64
  name: string
79
- type: any // @voxgig/struct validation schema
65
+ type: any
80
66
  req: boolean
81
67
  op: Partial<Record<OpName, ModelFieldOp>>
82
68
 
83
- // One-line human description, straight from the spec's property
84
- // `description`. Absent when the spec does not describe the property —
85
- // generators render an empty cell rather than inventing prose.
86
69
  short?: string
87
70
 
88
- // SPEC FACTS ABOUT THE FIELD ITSELF, carried through verbatim from the
89
- // OpenAPI property. Facts the spec states, not inferences.
90
- //
91
- // `readOnly` is the load-bearing one: it is the difference between a field
92
- // a client MAY send and one it may not, which nothing else in this record
93
- // expresses. Without it every generator necessarily puts server-assigned
94
- // fields into the type a caller fills in.
95
- //
96
- // The booleans are present ONLY when the spec declares them true — each
97
- // defaults to false in OpenAPI, so absent and explicit-false mean the same
98
- // thing and emitting the false ones would change every model for no
99
- // information. `format` is present only for a non-empty string.
100
71
  readOnly?: boolean
101
72
  writeOnly?: boolean
102
73
  deprecated?: boolean
103
74
  format?: string
104
75
  union?: {
105
- count: number // how many untagged unions lie beneath the field
106
- branches: number // widest branch count among them
107
- depth: number // how far down the widest one sits
76
+ count: number
77
+ branches: number
78
+ depth: number
108
79
  }
109
80
  }
110
81
 
@@ -117,7 +88,7 @@ type ModelField = {
117
88
  type ModelArg = {
118
89
  name: string
119
90
  orig: string
120
- type: any // @voxgig/struct validation schema
91
+ type: any
121
92
  kind: ArgKind
122
93
  reqd: boolean
123
94
  example?: any
@@ -131,8 +102,6 @@ type ModelArg = {
131
102
  type PointKind = 'http' | 'graphql'
132
103
 
133
104
 
134
- // Pagination descriptor for a GraphQL list op. `nodes`/`cursor`/`more` are
135
- // dotted paths relative to the unwrapped connection object.
136
105
  type ModelGraphqlPage = {
137
106
  style: string
138
107
  nodes: string
@@ -141,9 +110,6 @@ type ModelGraphqlPage = {
141
110
  }
142
111
 
143
112
 
144
- // One GraphQL variable binding: `name` is the variable as it appears in the
145
- // operation document, `from` the op argument it is read from, `gqltype` the
146
- // declared GraphQL type (e.g. 'String!').
147
113
  type ModelGraphqlVar = {
148
114
  name: string
149
115
  from: string
@@ -151,9 +117,6 @@ type ModelGraphqlVar = {
151
117
  }
152
118
 
153
119
 
154
- // GraphQL wire data for a point. `doc` is the complete operation document,
155
- // rendered single-line with sorted selection fields so output stays
156
- // byte-stable and schema drift shows up in model diffs.
157
120
  type ModelGraphql = {
158
121
  optype: 'query' | 'mutation'
159
122
  field: string
@@ -163,13 +126,6 @@ type ModelGraphql = {
163
126
  }
164
127
 
165
128
 
166
- // One concrete endpoint that can satisfy an operation. An entity op
167
- // (load/list/create/...) carries an array of these — apidef chooses
168
- // between them at runtime via `select.exist` matching against reqmatch /
169
- // reqdata. (Originally named `ModelTarget`; renamed for consistency with
170
- // the field name `points` and the runtime utility `MakePoint`.)
171
- // One resolved path segment (ADR-003). Exactly one of `lit` / `var` is set;
172
- // `var` names an entry of the point's `args.params`.
173
129
  type ModelPathSegment = {
174
130
  lit?: string
175
131
  var?: string
@@ -206,24 +162,12 @@ type ModelPoint = {
206
162
  }
207
163
 
208
164
 
209
- // Operation definition
210
165
  type ModelOp = {
211
166
  name: OpName
212
167
  points: ModelPoint[]
213
168
  }
214
169
 
215
170
 
216
- // Entity definition - core model entity with operations and fields.
217
- // `id` is present only when the OpenAPI response/request schema declares
218
- // (or examples imply) an `id` field on the entity. Public APIs that return
219
- // payloads without an id (e.g. read-only feeds) leave it undefined.
220
- //
221
- // `Name`, `NAME` etc. are stamped on by jostraca's `names()` helper after
222
- // apidef hands the model to the generator. They're typed as optional here
223
- // so apidef's transform code can construct entities without them; template
224
- // code should reach for them through `nom(entity, 'Name')` rather than
225
- // direct property access, which both works pre-`names()` and lets us
226
- // remove the optional later.
227
171
  type ModelEntity = {
228
172
  name: string
229
173
  Name?: string
@@ -233,28 +177,8 @@ type ModelEntity = {
233
177
  id?: {
234
178
  name: string
235
179
  field: string
236
- // COMPOSITE IDENTITY. Present only when the API addresses one record by
237
- // MORE THAN ONE path parameter, so no single parameter is the id.
238
- // github's repo is the case: GET /repos/{owner}/{repo} needs both, and
239
- // neither alone names a repository.
240
- //
241
- // `parts` are those parameters in path order; `sep` joins them into the
242
- // one `id` an SDK entity carries. Absent means the ordinary single-key
243
- // entity, so downstream can branch on presence alone.
244
180
  parts?: string[]
245
181
  sep?: string
246
- // WHERE EACH PART'S VALUE LIVES IN A RESPONSE, as a dotted path.
247
- //
248
- // The parts are PATH PARAMETER names and a response names its fields
249
- // whatever it likes: github addresses a repo by `{owner}/{repo}` and
250
- // returns the owner as an OBJECT, so the value is at `owner.login`, and
251
- // the repository under `name`. Without this a consumer can address a
252
- // record it was given the id of, but cannot put an id on a record the
253
- // API returned.
254
- //
255
- // A part no rule resolves is left OUT, so an incomplete map says the id
256
- // cannot be rebuilt for that entity — better than a confidently wrong id
257
- // on a real record. guide.aon can state it instead.
258
182
  from?: Record<string, string>
259
183
  }
260
184
  relations: ModelEntityRelations
@@ -265,7 +189,6 @@ type ModelEntityFlow = {
265
189
  name: string,
266
190
  entity: string
267
191
  kind: string
268
- // args: Record<string, string>
269
192
  step: ModelEntityFlowStep[]
270
193
  active?: boolean
271
194
  }
@@ -291,8 +214,6 @@ type ModelEntityFlowStepInput = {
291
214
  }
292
215
 
293
216
 
294
- // Validators and specs are user-supplied callables identified by the
295
- // `apply` discriminator; `def` is the validator-specific options bag.
296
217
  type ModelEntityFlowStepValidator = {
297
218
  apply: string
298
219
  def: Record<string, any>