@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
@@ -50,29 +50,6 @@ const fieldTransform: Transform = async function(
50
50
  const mpoints = mop.points
51
51
 
52
52
  for (let mpoint of mpoints) {
53
- // AN ACTION POINT IS A VERB: its request body is that verb's
54
- // arguments and its response is that verb's result, so neither
55
- // describes a record of the entity. solar's planet -- four
56
- // properties in the spec -- came out with ten fields, the extra six
57
- // being `{start, stop}` and `{forbid, why}` from the two action
58
- // bodies and `{ok, state}` from their shared response envelope.
59
- // Those reached the generated `Planet` type, its create and update
60
- // data types, and the per-entity field table in the generated
61
- // reference, none of which a planet has ever carried.
62
- //
63
- // THE EXCLUSION BELONGS WITH THE SCHEMAS, not here. `findFieldDefs`
64
- // drops an action's request body outright and keeps its response
65
- // only when that response is the entity's OWN component --
66
- // `/v2/installments/active` returning `[Installment]` is,
67
- // `uploadImage` returning `ApiResponse` is not -- and on the
68
- // graphql path the question never arises, because the fields come
69
- // from the entity's own object type rather than from any response.
70
- //
71
- // Deciding it here instead cost every entity whose points are ALL
72
- // actions its entire field list: 24 across the validate corpus,
73
- // from github's graphql `commit` and `team` to learnworlds'
74
- // `installment` and shopify's `mailing_address`, each left with a
75
- // generated type carrying no fields at all.
76
53
  const opfields = resolveOpFields(ment, mop, mpoint, def)
77
54
 
78
55
  for (let opfield of opfields) {
@@ -92,42 +69,11 @@ const fieldTransform: Transform = async function(
92
69
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
93
70
  })
94
71
 
95
- // Mark the entity as having an id only when the spec actually declares one.
96
- // Downstream (test generators, fixture builders) gate id-specific code on
97
- // this presence so that public read-only APIs without ids don't get
98
- // bogus id assertions.
99
- // COMPOSITE FIRST, because a compound key need not come with an `id`.
100
- //
101
- // An entity addressed by `{owner}/{repo}` whose response carries only
102
- // `owner` and `name` has no field literally named `id`, and its adjacent
103
- // placeholders are left unrenamed so `addressedById` is false too.
104
- // Neither branch below then ran, so the entity got NO id descriptor and
105
- // even an explicit `guide.entity.<name>.id.parts` was silently ignored —
106
- // while the Go port, which initialises a descriptor unconditionally,
107
- // emitted the composite. The ports disagreed on exactly the shape this
108
- // feature exists for.
109
72
  const gent = guide?.entity?.[ment.name]
110
73
  const composite = compositeId(ment, gent, def)
111
74
 
112
75
  const idField = fields.find((f: ModelField) => 'id' === f.name)
113
76
 
114
- // A COMPOSITE ID IS A STRING, whatever the API's own `id` field is —
115
- // AND THE API'S OWN id IS KEPT.
116
- //
117
- // github's repo declares `id` as an integer, its global database id,
118
- // while the composite identity is `owner/repo`. Two facts have to
119
- // survive: `id` must hold a string, because that is what the joined
120
- // value is and what every generated type has to store; and the spec's
121
- // numeric property must not be silently reinterpreted, because a
122
- // consumer that wants the database id is entitled to it with its own
123
- // type and format intact.
124
- //
125
- // So the API's field MOVES to `<api>_id` rather than being rewritten in
126
- // place, carrying its type, format and per-op overrides with it, and the
127
- // entity's `alias.field` map records where it went. Retyping in place
128
- // (the first attempt) claimed the server's numeric id was a string;
129
- // leaving it alone made `id.field` name a declaration the runtime value
130
- // cannot satisfy. Moving it is the only option that lies about neither.
131
77
  if (null != composite.parts && null != idField && !scalarStringField(idField)) {
132
78
  const idf: any = idField
133
79
  const apiname = String((model as any)?.name || 'api')
@@ -193,12 +139,6 @@ const fieldTransform: Transform = async function(
193
139
  ment.id = { name: 'id', field: 'id', ...composite }
194
140
  }
195
141
  else if (addressedById(ment)) {
196
- // The FIELD as well as the descriptor. An entity addressed by id has an
197
- // id at runtime — the test fixture seeds one, and the SDK sends it — so
198
- // a model that declares the descriptor without the field makes the
199
- // generated TYPE disagree with the generated TEST: trello's Option,
200
- // Reaction and Sticker compiled to `TS2339: Property 'id' does not
201
- // exist` the moment the test started assigning data.id.
202
142
  fields.push({
203
143
  name: 'id',
204
144
  type: '`$STRING`',
@@ -207,26 +147,6 @@ const fieldTransform: Transform = async function(
207
147
  fields.sort((a: ModelField, b: ModelField) =>
208
148
  a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
209
149
 
210
- // ADDRESSABLE BY ID WITHOUT DECLARING ONE AS A FIELD.
211
- //
212
- // The rule above reads the RESPONSE schema, and plenty of real entities
213
- // are addressed by an id their response never repeats. github's
214
- // private_registry is one: PATCH /orgs/{org}/private-registries/{secret_name}
215
- // renames secret_name to id, so the entity is addressed by id on every
216
- // one of its own routes, while its schema declares only created_at, key,
217
- // name, url and friends.
218
- //
219
- // Downstream that absence is not cosmetic. TestEntity gates
220
- // `data.id = <created>.id` on THIS descriptor, so the generated update
221
- // carried no id at all, the test mock's selector fell back to whatever
222
- // else was in reqdata (org_id), matched no single record, and the flow
223
- // failed with a 404 that named nothing to do with ids.
224
- //
225
- // An entity whose own points take an `id` param IS addressable by id;
226
- // that is the property the downstream generators actually want. Entities
227
- // with neither a field nor an id param — the read-only public APIs the
228
- // rule above was written for — still get no descriptor, so they still
229
- // get no id assertions.
230
150
  ment.id = { name: 'id', field: 'id', ...composite }
231
151
  }
232
152
 
@@ -238,14 +158,6 @@ const fieldTransform: Transform = async function(
238
158
 
239
159
 
240
160
 
241
- // The separator that joins a composite id into one string.
242
- //
243
- // A forward slash cannot occur inside a single path segment — a raw `/`
244
- // would end the segment, and a value that legitimately contains one arrives
245
- // percent-encoded as `%2F` — so joining on it can never be ambiguous, and
246
- // splitting on it can never over-split. That is what makes the composite id
247
- // safe to carry as a single opaque string, which is the property the SDK and
248
- // Seneca entities are built on.
249
161
  const ID_SEP = '/'
250
162
 
251
163
  // Subfields that conventionally carry the identifying value of a nested
@@ -261,36 +173,6 @@ const NESTED_ID_KEYS = ['login', 'slug', 'name', 'key', 'id']
261
173
  const ID_OPS = ['load', 'update', 'patch', 'remove']
262
174
 
263
175
 
264
- // The parameters that TOGETHER name one record: the trailing run of
265
- // ADJACENT variable segments on the addressing route.
266
- //
267
- // ADJACENCY IS THE WHOLE TEST, and it is what separates a compound key from
268
- // ordinary parent/child nesting:
269
- //
270
- // /repos/{owner}/{repo} -> owner, repo COMPOSITE
271
- // /api/planet/{planet_id}/moon/{moon_id} -> moon_id single
272
- // /repos/{owner}/{repo}/pulls/{pull_number} -> pull_number single
273
- //
274
- // A literal segment between two variables names a SUB-COLLECTION, so the
275
- // earlier variable scopes the later one — `planet_id` says which planet's
276
- // moons, and `moon_id` alone identifies the moon. Two variables with nothing
277
- // between them address no sub-collection: neither value names anything on
278
- // its own, and only the pair identifies a repository.
279
- //
280
- // Taking every variable on the path instead was tried first and is wrong on
281
- // most real specs — it made `moon` (planet_id + moon_id), petstore's `order`,
282
- // `pet` and `user`, and taxonomy's `domain` and `kingdom` all falsely
283
- // composite, which the apidef-validate goldens caught immediately. Nested
284
- // resources are the common shape; compound keys are the exception, and
285
- // adjacency is the thing that actually distinguishes them.
286
- //
287
- // Read from the op that names a single record, never from `list`: a
288
- // collection route's path params are the entity's parents. A point ending in
289
- // a literal is a verb ON the record (`.../{number}/merge`) and carries the
290
- // same variables, so it is a fallback rather than a different answer.
291
- // Walk back from a point's end, collecting variables until a literal stops
292
- // the run. That literal is the sub-collection boundary; anything before it
293
- // scopes this record rather than naming it.
294
176
  function trailingVars(point: any): string[] {
295
177
  const segs = ((point?.segments || []) as any[]).filter((s: any) => null != s)
296
178
  const run: string[] = []
@@ -307,15 +189,6 @@ function trailingVars(point: any): string[] {
307
189
 
308
190
 
309
191
  function identityParams(ment: ModelEntity): string[] {
310
- // EVERY ID-BEARING OP AT ONCE, not the first one that offers a candidate.
311
- //
312
- // These four ops all address a single record, so all four describe the
313
- // same identity — but they do not all carry the same routes. gitlab's
314
- // `project` has `/api/v4/projects/{id}` under `remove` alone, while its
315
- // `load` carries only sub-resources like
316
- // `/api/v4/projects/{id}/uploads/{secret}/{filename}`. Returning on the
317
- // first op with any candidate therefore made a PROJECT identified by
318
- // `secret/filename`. The op order is now only a tie-break.
319
192
  const cands: any[] = []
320
193
 
321
194
  for (let o = 0; o < ID_OPS.length; o++) {
@@ -337,22 +210,6 @@ function identityParams(ment: ModelEntity): string[] {
337
210
  run,
338
211
  // Segments BEFORE the run: how much parent scope the route needs.
339
212
  scope: ((pt.segments || []).length - run.length),
340
- // DOES THE RUN END IN THE RECORD'S OWN KEY? Then it is the
341
- // record's address and nothing further is needed.
342
- //
343
- // This transform RENAMES that parameter to `id`, so a run ending in
344
- // it is this port's own statement of what identifies the record —
345
- // and the composite inference must not contradict it.
346
- // `/gists/{gist_id}` becomes `/gists/{id}` and is a gist;
347
- // `/gists/{gist_id}/{sha}` is a REVISION of one, and won on key
348
- // length alone, so a gist came out keyed `gist_id/sha` while the
349
- // generated SDK's own load match takes the single parameter. The
350
- // same contradiction gave cloudsmith's repo and vulnerability
351
- // compound keys their SDKs never address them by.
352
- //
353
- // Deliberately narrow: exactly `id` or an unrenamed `<entity>_id`,
354
- // never any `*_id`. `actor_type/actor_id` IS a compound key, and a
355
- // looser test breaks it.
356
213
  own: 'id' === run[run.length - 1] ||
357
214
  (ment as any).name + '_id' === run[run.length - 1],
358
215
  order: o,
@@ -360,34 +217,6 @@ function identityParams(ment: ModelEntity): string[] {
360
217
  }
361
218
  }
362
219
 
363
- // WHICH ROUTE IS THE RECORD'S OWN ADDRESS.
364
- //
365
- // An entity gathers every route that reads it, and in a large
366
- // specification most of those are sub-resources. Three earlier rules were
367
- // measured against the validation corpus, and each is wrong:
368
- //
369
- // The FIRST route listed gave github's `repo` the single part
370
- // `subject_digest`, from
371
- // `/repos/{owner}/{repo}/attestations/{subject_digest}` — no compound
372
- // key at all, for the entity this feature exists for. Invisible on a
373
- // small spec, where the first item route IS the record's own.
374
- //
375
- // The SHORTEST route ending in a variable took cloudsmith's
376
- // `/vulnerabilities/{owner}/` — a LIST of an owner's vulnerabilities —
377
- // and cut a four-part key down to `owner`, dropping three more
378
- // composites. Ending in a variable does not make a route an address.
379
- //
380
- // The LONGEST trailing run took
381
- // `/orgs/{org}/teams/{team_slug}/repos/{owner}/{repo}` and made a TEAM
382
- // identified by `owner/repo`. A deep sub-resource can carry more
383
- // adjacent variables than the record's own route does.
384
- //
385
- // What separates them is PARENT SCOPE: the record's own route is the
386
- // least-qualified one that names it, and among equally-qualified routes
387
- // the one carrying the fullest key. `/repos/{owner}/{repo}` is qualified
388
- // by one segment and the attestations route by four; `/teams/{team_id}`
389
- // by one and the org-team-repo route by five; cloudsmith's vulnerability
390
- // routes are all qualified by one, so the fullest of them wins.
391
220
  const best = cands.reduce((b: any, c: any) => {
392
221
  if (null == b) {
393
222
  return c
@@ -407,34 +236,6 @@ function identityParams(ment: ModelEntity): string[] {
407
236
  return null == best ? [] : best.run
408
237
  }
409
238
 
410
- // THE PROPERTY MAPS A RESPONSE COULD BE DESCRIBING, best first.
411
- //
412
- // BOTH SPEC DIALECTS. An OpenAPI 3 response carries its schema under
413
- // `content['application/json']`; a SWAGGER 2 response carries it directly as
414
- // `schema`. Reading only the first resolved nothing for every Swagger 2 spec
415
- // in the validation corpus.
416
- //
417
- // JSON ONLY, where there is a choice. An operation may declare several media
418
- // types with different schemas, and field extraction uses the JSON one — so
419
- // picking whichever came first in source order could infer a path from an XML
420
- // or binary schema that the actual JSON record does not have.
421
- //
422
- // `allOf` IS EXPANDED, because a response that composes its entity that way
423
- // has neither `properties` nor `items` of its own. field extraction expands
424
- // it; not doing so here meant the fields were present while the id could not
425
- // be reconstructed.
426
- //
427
- // ONLY THE ENVELOPE IS DESCENDED, via the same `envelopeProp` rule field
428
- // extraction uses. Descending every object-valued property instead treats an
429
- // ordinary nested object as a whole record: for `{ slug, metadata: { tenant } }`
430
- // addressed by `{tenant}/{slug}`, `tenant` resolved to `tenant` rather than
431
- // `metadata.tenant` — a confidently wrong path, which is worse than no
432
- // mapping at all.
433
- //
434
- // ACTION POINTS ARE SKIPPED, as `identityParams` skips them: an action's
435
- // response is a verb's result, not a representation of the entity, so a field
436
- // that happens to appear there says nothing about what a returned record
437
- // carries.
438
239
  function responseCandidates(ment: ModelEntity, def: any): any[] {
439
240
  const out: any[] = []
440
241
  const seen = new Set<any>()
@@ -515,7 +316,6 @@ function responseCandidates(ment: ModelEntity, def: any): any[] {
515
316
  }
516
317
  }
517
318
 
518
- // Swagger 2 puts it here.
519
319
  add(resdef.schema, opname)
520
320
  }
521
321
  }
@@ -526,11 +326,6 @@ function responseCandidates(ment: ModelEntity, def: any): any[] {
526
326
 
527
327
 
528
328
 
529
- // Does this schema name the entity's own component?
530
- //
531
- // The comparison is on the CANONICALISED component name, the same function
532
- // the guide used to derive an entity name from a component in the first
533
- // place, so `Installment` and the entity `installment` meet.
534
329
  function namesEntity(schema: any, ment: ModelEntity): boolean {
535
330
  const xref = schema?.['x-ref']
536
331
 
@@ -607,12 +402,6 @@ function partAliases(ment: ModelEntity, part: string): string[] {
607
402
  }
608
403
 
609
404
 
610
- // Where one part is carried in a given property map, or null.
611
- //
612
- // The four rules, in order, each a fact the spec states: a scalar property of
613
- // that name; the part naming this entity, resolved to `name`; a scalar
614
- // `<part>_name` / `_login` / `_slug`; or an object property's conventional
615
- // identifying subfield.
616
405
  function resolvePart(
617
406
  ment: ModelEntity,
618
407
  part: string,
@@ -661,52 +450,15 @@ function resolvePart(
661
450
  }
662
451
 
663
452
 
664
- // WHERE EACH COMPOSITE PART'S VALUE LIVES IN A RESPONSE.
665
- //
666
- // The parts are PATH PARAMETER names; a response names its fields whatever it
667
- // likes. Resolving one to the other is what lets an SDK put an id on a record
668
- // the API returned, rather than only address a record whose id it was given.
669
- //
670
- // The rules, in order, and each of them is a fact about the spec rather than
671
- // a guess:
672
- //
673
- // 1. a scalar field of exactly that name -> itself
674
- // 2. the part names this entity, and there is a `name` -> `name`
675
- // (`/repos/{owner}/{repo}` on entity `repo`, whose response calls the
676
- // repository `name`)
677
- // 3. a scalar `<part>_name` / `<part>_login` / `<part>_slug`
678
- // 4. an OBJECT field of that name -> `<part>.<conventional key>`
679
- // (`owner` is a user object; the value is `owner.login`)
680
- //
681
- // A part none of these resolve is left OUT. Downstream then knows the id
682
- // cannot be rebuilt for that entity and can say so, which is better than a
683
- // confidently wrong id on a real record. guide.aon can state it instead.
684
453
  function identityFrom(
685
454
  ment: ModelEntity,
686
455
  parts: string[],
687
456
  def: any,
688
457
  ): Record<string, string> {
689
- // THE RESPONSE SCHEMA IS THE AUTHORITY, not `ment.fields`.
690
- //
691
- // `ment.fields` is merged across load, create, update and list, so a part
692
- // that exists only in a REQUEST BODY appears there too. Resolving against
693
- // it recorded such a part in `from` as though a returned record carried it,
694
- // and a consumer then rebuilt an id from a property the response never
695
- // sends — worse than leaving the part unresolved, which at least says so.
696
- //
697
- // Candidate property maps, in order: the response's own properties, then
698
- // one level into an envelope. A response that wraps the record
699
- // (`{ item: {...} }`, `{ data: [ {...} ] }`) states the record's fields one
700
- // level in, and searching only the wrapper found nothing.
701
458
  const candidates = responseCandidates(ment, def)
702
459
  const out: Record<string, string> = {}
703
460
 
704
461
  for (const part of parts) {
705
- // THE WIRE NAME AS WELL AS THE MODEL NAME. `identityParams` reads the
706
- // RENAMED parameter off the path segments, while a response keeps its own
707
- // casing — so a `tenantKey` renamed to `tenant_key` was looked up under a
708
- // name the response does not use, and the mapping was dropped for every
709
- // camel-cased or depluralized parameter.
710
462
  const aliases = partAliases(ment, part)
711
463
 
712
464
  let found: string | null = null
@@ -734,15 +486,6 @@ function scalarStringField(f: any): boolean {
734
486
  }
735
487
 
736
488
 
737
- // WHICH PARAMETER IS THE RECORD'S OWN KEY, among several that looked
738
- // adjacent. The same shape apidef's id handling recognises everywhere else:
739
- //
740
- // 1. one named exactly `id`
741
- // 2. `<entity>_id` — the entity's own id, however the path spells it
742
- // 3. any `*_id` — an id by name
743
- // 4. failing all that, the terminal parameter
744
- //
745
- // Position is the LAST resort, not the first.
746
489
  function singleKeyOf(ment: ModelEntity, parts: string[]): string | undefined {
747
490
  if (0 === parts.length) {
748
491
  return undefined
@@ -755,12 +498,6 @@ function singleKeyOf(ment: ModelEntity, parts: string[]): string | undefined {
755
498
  }
756
499
 
757
500
 
758
- // The composite half of the id descriptor, or `{}` for the ordinary case.
759
- //
760
- // Emitted ONLY for a genuinely composite id (two or more addressing
761
- // parameters). A single-parameter entity already round-trips through one
762
- // `id` and gains nothing from carrying a one-element `parts`, so its
763
- // descriptor is left exactly as it was — no existing model output moves.
764
501
  function compositeId(
765
502
  ment: ModelEntity,
766
503
  gent?: any,
@@ -773,17 +510,6 @@ function compositeId(
773
510
  // empty `parts`, because aontu resolves an empty list to nothing and the
774
511
  // key would arrive absent — indistinguishable from never having been set.
775
512
  if (null != gid && false === gid.composite) {
776
- // DISABLING COMPOSITE MUST NOT DISABLE THE ID. The correction says these
777
- // adjacent parameters are not a compound key; it does not say the record
778
- // has no key. Returning a bare `{}` left an entity whose response has no
779
- // literal `id` with no descriptor at all — the false positive removed and
780
- // nothing identifying the real key.
781
- //
782
- // WHICH of the adjacent parameters is that key is decided by the same
783
- // id-finding rules apidef uses elsewhere, not by position. Taking the
784
- // terminal one picked `archive_format` for
785
- // `/artifacts/{artifact_id}/{archive_format}` — the modifier, precisely
786
- // the false positive the correction exists to undo.
787
513
  return { single: singleKeyOf(ment, identityParams(ment)) } as any
788
514
  }
789
515
 
@@ -858,19 +584,6 @@ function resolveOpFields(
858
584
  req: !!fielddef.required,
859
585
  op: {},
860
586
  }
861
- // Carry the spec's own words for the field, when it has any.
862
- //
863
- // Every generated per-entity table has a Description column and every cell
864
- // was blank, because nothing ever read the property `description` the spec
865
- // supplies. Trimmed, and only when it is a non-empty string: a whitespace
866
- // or non-string value would put a meaningless cell where an empty one is
867
- // honest.
868
- // ONE LINE, not the whole description. Every generated Readme drops this
869
- // straight into a markdown table cell, where a raw newline ends the row
870
- // and orphans the rest of the table — and specs put bullet lists, fenced
871
- // examples and multi-paragraph notes in `description`. firstSentence is
872
- // the same reduction the API summary uses, so `short` means the same
873
- // thing wherever it appears.
874
587
  const fdesc = (fielddef as any).description
875
588
  if ('string' === typeof fdesc && '' !== fdesc.trim()) {
876
589
  const short = firstSentence(fdesc)
@@ -879,20 +592,6 @@ function resolveOpFields(
879
592
  }
880
593
  }
881
594
 
882
- // SPEC FACTS ABOUT THE FIELD, carried through verbatim.
883
- //
884
- // These four are declared by OpenAPI on the property and were being
885
- // dropped on the floor. `readOnly` is the one that matters most: it is
886
- // the difference between a field a client MAY send and one it may not,
887
- // and nothing else in the model says which — so every generator has been
888
- // putting server-assigned fields into the type a caller fills in.
889
- //
890
- // ONLY WHEN THE SPEC SAYS SO, and for the booleans only when TRUE. Each
891
- // defaults to false in OpenAPI, so an absent key and an explicit `false`
892
- // carry the same information; emitting the false ones would add a key to
893
- // every field of every model and say nothing. Same discipline as
894
- // `short`: absent means "the spec did not say", never "apidef dropped
895
- // it".
896
595
  for (const flag of ['readOnly', 'writeOnly', 'deprecated'] as const) {
897
596
  if (true === (fielddef as any)[flag]) {
898
597
  mfield[flag] = true
@@ -942,7 +641,6 @@ function findGraphqlFieldDefs(
942
641
 
943
642
  const out: SchemaDef[] = []
944
643
 
945
- // Sorted by construction in parse/graphql.ts, so output stays byte-stable.
946
644
  for (const fname of Object.keys(gtype.fields)) {
947
645
  const f = gtype.fields[fname]
948
646
 
@@ -965,17 +663,10 @@ function findGraphqlFieldDefs(
965
663
  // custom scalars left unconstrained.
966
664
  type: 'ENUM' === kind ? 'string' : gqlFieldType(f.type),
967
665
  required: f.reqd,
968
- // GraphQL puts the field's own words on GqlField.desc (see
969
- // parse/graphql.ts). resolveOpFields reads `description`, the OpenAPI
970
- // spelling, so name it that here rather than teaching the reader two.
971
666
  description: f.desc,
972
667
  } as any)
973
668
  }
974
669
  else if (('OBJECT' === kind || 'INTERFACE' === kind) && !f.list) {
975
- // To-one relation. The default fragment selects `team { id }`, so the
976
- // response carries a nested stub object — declare it as such. Naming a
977
- // flat `team_id` here would advertise a field the wire never returns,
978
- // since nothing flattens the response.
979
670
  const idField = ftype.fields?.id
980
671
  if (null != idField) {
981
672
  out.push({
@@ -992,12 +683,6 @@ function findGraphqlFieldDefs(
992
683
  }
993
684
 
994
685
 
995
- // GraphQL named type -> the type names the field typing understands.
996
- //
997
- // Built-ins only: a custom scalar (JSON, JSONObject, Upload, ...) can hold
998
- // any JSON value, so advertising it as a string would misdescribe the data
999
- // and make generated validation reject values the schema accepts. Enums are
1000
- // mapped by the caller, which knows they are strings.
1001
686
  function gqlFieldType(typeName: string): string | undefined {
1002
687
  return 'Int' === typeName ? 'integer' :
1003
688
  'Float' === typeName ? 'number' :
@@ -1021,10 +706,11 @@ function findFieldDefs(
1021
706
  const isAction = null != (mpoint as any)?.select?.['$action']
1022
707
 
1023
708
  const fielddefs: SchemaDef[] = []
709
+
1024
710
  const pathdef = def.paths[mpoint.orig]
1025
711
 
1026
712
  const method = mpoint.method.toLowerCase()
1027
- const opdef: any = pathdef[method]
713
+ const opdef: any = (pathdef as any)?.[method]
1028
714
 
1029
715
  if (opdef) {
1030
716
  const responses = opdef.responses
@@ -1036,13 +722,6 @@ function findFieldDefs(
1036
722
  fieldSets = getx(responses, '200 content "application/json" schema') ??
1037
723
  getx(responses, '200 schema')
1038
724
  if ('list' == mop.name) {
1039
- // List responses commonly come in three shapes:
1040
- // 1. direct array — { type: array, items: { ...item } }
1041
- // 2. wrapper object — { properties: { items: [Item], page, ... } }
1042
- // (a single array-of-object property inside an object schema)
1043
- // 3. legacy "list of created items" under 201
1044
- // Resolve to the inner item schema when we can identify one
1045
- // unambiguously; otherwise fall through to the 200 schema as-is.
1046
725
  const unwrapped = unwrapArrayWrapper(fieldSets)
1047
726
  if (unwrapped) {
1048
727
  fieldSets = unwrapped
@@ -1058,14 +737,6 @@ function findFieldDefs(
1058
737
  getx(responses, '201 schema')
1059
738
  }
1060
739
 
1061
- // Single-entity responses get the same treatment the list branch above
1062
- // already gives collections: a body that is only an envelope around the
1063
- // entity — `{item: {...}}` — describes the WRAPPER, not the entity, so
1064
- // its sole property would otherwise be harvested as a field. That is
1065
- // how an entity `todoitem` ended up with a required `item` field of
1066
- // type object, which then appeared in the generated create/update data
1067
- // types. envelopeProp applies the same two rules used to pick the
1068
- // response transform, so the field list and the transform agree.
1069
740
  if ('list' != mop.name) {
1070
741
  const envelope = envelopeProp(fieldSets?.properties, mop.name)
1071
742
  if (null != envelope) {
@@ -1074,15 +745,6 @@ function findFieldDefs(
1074
745
  }
1075
746
  }
1076
747
 
1077
- // AN ACTION'S RESPONSE IS THE VERB'S RESULT -- unless it is the entity.
1078
- //
1079
- // `POST /pet/{petId}/uploadImage` answers with an `ApiResponse`, which
1080
- // says how the upload went; `GET /v2/installments/active` answers with
1081
- // `[Installment]`, which is what an installment IS. Both are actions, so
1082
- // the verb alone cannot tell them apart -- but the component can, and
1083
- // parse.ts keeps it: every resolved `$ref` leaves its original pointer
1084
- // behind as `x-ref`, so the component a response was written against
1085
- // survives inlining and can be compared with the entity that owns it.
1086
748
  if (isAction && !namesEntity(fieldSets, ment)) {
1087
749
  return fielddefs
1088
750
  }
@@ -1190,7 +852,6 @@ function findExampleObject(opdef: any): any {
1190
852
  example = getx(resdef, 'examples "application/json"')
1191
853
  if (null != example && 'object' === typeof example) return unwrapExample(example)
1192
854
 
1193
- // Swagger 2.0: schema.example
1194
855
  example = getx(resdef, 'schema example')
1195
856
  if (null != example && 'object' === typeof example) return unwrapExample(example)
1196
857
 
@@ -1207,16 +868,6 @@ function unwrapExample(example: any): any {
1207
868
  }
1208
869
 
1209
870
 
1210
- // unwrapArrayWrapper inspects a list-response schema and, when it is an
1211
- // object with a single array-of-object-schema property (e.g.
1212
- // { boards: [Board] }, { items: [Foo], page, total, ... }), returns the
1213
- // inner item schema so that field resolution sees the actual entity
1214
- // properties rather than the wrapper's bookkeeping.
1215
- //
1216
- // Returns null if the input is not unambiguously such a wrapper:
1217
- // - schema is already an array → return null (let caller use it directly)
1218
- // - no array-of-object-schema property → return null
1219
- // - more than one array-of-object-schema property → ambiguous, return null
1220
871
  function unwrapArrayWrapper(schema: any): any {
1221
872
  if (null == schema || 'object' !== typeof schema) return null
1222
873
  // Direct list shape — caller can resolve from items directly.
@@ -1268,27 +919,10 @@ function mergeField(
1268
919
  }
1269
920
  }
1270
921
 
1271
- // Field identity is first-writer-wins, but a DESCRIPTION is not part of
1272
- // identity: the op that first names a field is often not the one that
1273
- // documents it (a load response referencing a bare component, a create body
1274
- // referencing the annotated one). Take the first non-empty description in
1275
- // opFieldPrecedence order and keep it — dropping it left a blank cell in
1276
- // every generated table while the spec had the words all along.
1277
922
  if (null == existingField.short && null != newField.short) {
1278
923
  existingField.short = newField.short
1279
924
  }
1280
925
 
1281
- // The spec facts merge the same way, and for the same reason: one schema
1282
- // annotates the field and another references it bare, so taking the first
1283
- // declaration in opFieldPrecedence order is what finds the annotation.
1284
- //
1285
- // THE PRECEDENCE ORDER PUTS `load` FIRST, WHICH IS THE SAFE DIRECTION HERE.
1286
- // A field the response schema marks readOnly and a request body also lists
1287
- // is a self-contradictory spec — OpenAPI says a client must not send a
1288
- // readOnly property at all — and this resolves it by believing the
1289
- // restriction rather than the omission. Marking a writable field readOnly
1290
- // costs a caller one field; the other way round sends a value the server
1291
- // rejects.
1292
926
  for (const flag of ['readOnly', 'writeOnly', 'deprecated', 'format'] as const) {
1293
927
  if (null == existingField[flag] && null != newField[flag]) {
1294
928
  (existingField as any)[flag] = newField[flag]