@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
@@ -19,29 +19,6 @@ const fieldTransform = async function (ctx) {
19
19
  if (mop) {
20
20
  const mpoints = mop.points;
21
21
  for (let mpoint of mpoints) {
22
- // AN ACTION POINT IS A VERB: its request body is that verb's
23
- // arguments and its response is that verb's result, so neither
24
- // describes a record of the entity. solar's planet -- four
25
- // properties in the spec -- came out with ten fields, the extra six
26
- // being `{start, stop}` and `{forbid, why}` from the two action
27
- // bodies and `{ok, state}` from their shared response envelope.
28
- // Those reached the generated `Planet` type, its create and update
29
- // data types, and the per-entity field table in the generated
30
- // reference, none of which a planet has ever carried.
31
- //
32
- // THE EXCLUSION BELONGS WITH THE SCHEMAS, not here. `findFieldDefs`
33
- // drops an action's request body outright and keeps its response
34
- // only when that response is the entity's OWN component --
35
- // `/v2/installments/active` returning `[Installment]` is,
36
- // `uploadImage` returning `ApiResponse` is not -- and on the
37
- // graphql path the question never arises, because the fields come
38
- // from the entity's own object type rather than from any response.
39
- //
40
- // Deciding it here instead cost every entity whose points are ALL
41
- // actions its entire field list: 24 across the validate corpus,
42
- // from github's graphql `commit` and `team` to learnworlds'
43
- // `installment` and shopify's `mailing_address`, each left with a
44
- // generated type carrying no fields at all.
45
22
  const opfields = resolveOpFields(ment, mop, mpoint, def);
46
23
  for (let opfield of opfields) {
47
24
  if (!seen[opfield.name]) {
@@ -58,40 +35,9 @@ const fieldTransform = async function (ctx) {
58
35
  fields.sort((a, b) => {
59
36
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
60
37
  });
61
- // Mark the entity as having an id only when the spec actually declares one.
62
- // Downstream (test generators, fixture builders) gate id-specific code on
63
- // this presence so that public read-only APIs without ids don't get
64
- // bogus id assertions.
65
- // COMPOSITE FIRST, because a compound key need not come with an `id`.
66
- //
67
- // An entity addressed by `{owner}/{repo}` whose response carries only
68
- // `owner` and `name` has no field literally named `id`, and its adjacent
69
- // placeholders are left unrenamed so `addressedById` is false too.
70
- // Neither branch below then ran, so the entity got NO id descriptor and
71
- // even an explicit `guide.entity.<name>.id.parts` was silently ignored —
72
- // while the Go port, which initialises a descriptor unconditionally,
73
- // emitted the composite. The ports disagreed on exactly the shape this
74
- // feature exists for.
75
38
  const gent = guide?.entity?.[ment.name];
76
39
  const composite = compositeId(ment, gent, def);
77
40
  const idField = fields.find((f) => 'id' === f.name);
78
- // A COMPOSITE ID IS A STRING, whatever the API's own `id` field is —
79
- // AND THE API'S OWN id IS KEPT.
80
- //
81
- // github's repo declares `id` as an integer, its global database id,
82
- // while the composite identity is `owner/repo`. Two facts have to
83
- // survive: `id` must hold a string, because that is what the joined
84
- // value is and what every generated type has to store; and the spec's
85
- // numeric property must not be silently reinterpreted, because a
86
- // consumer that wants the database id is entitled to it with its own
87
- // type and format intact.
88
- //
89
- // So the API's field MOVES to `<api>_id` rather than being rewritten in
90
- // place, carrying its type, format and per-op overrides with it, and the
91
- // entity's `alias.field` map records where it went. Retyping in place
92
- // (the first attempt) claimed the server's numeric id was a string;
93
- // leaving it alone made `id.field` name a declaration the runtime value
94
- // cannot satisfy. Moving it is the only option that lies about neither.
95
41
  if (null != composite.parts && null != idField && !scalarStringField(idField)) {
96
42
  const idf = idField;
97
43
  const apiname = String(model?.name || 'api');
@@ -146,38 +92,12 @@ const fieldTransform = async function (ctx) {
146
92
  ment.id = { name: 'id', field: 'id', ...composite };
147
93
  }
148
94
  else if (addressedById(ment)) {
149
- // The FIELD as well as the descriptor. An entity addressed by id has an
150
- // id at runtime — the test fixture seeds one, and the SDK sends it — so
151
- // a model that declares the descriptor without the field makes the
152
- // generated TYPE disagree with the generated TEST: trello's Option,
153
- // Reaction and Sticker compiled to `TS2339: Property 'id' does not
154
- // exist` the moment the test started assigning data.id.
155
95
  fields.push({
156
96
  name: 'id',
157
97
  type: '`$STRING`',
158
98
  req: false,
159
99
  });
160
100
  fields.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
161
- // ADDRESSABLE BY ID WITHOUT DECLARING ONE AS A FIELD.
162
- //
163
- // The rule above reads the RESPONSE schema, and plenty of real entities
164
- // are addressed by an id their response never repeats. github's
165
- // private_registry is one: PATCH /orgs/{org}/private-registries/{secret_name}
166
- // renames secret_name to id, so the entity is addressed by id on every
167
- // one of its own routes, while its schema declares only created_at, key,
168
- // name, url and friends.
169
- //
170
- // Downstream that absence is not cosmetic. TestEntity gates
171
- // `data.id = <created>.id` on THIS descriptor, so the generated update
172
- // carried no id at all, the test mock's selector fell back to whatever
173
- // else was in reqdata (org_id), matched no single record, and the flow
174
- // failed with a 404 that named nothing to do with ids.
175
- //
176
- // An entity whose own points take an `id` param IS addressable by id;
177
- // that is the property the downstream generators actually want. Entities
178
- // with neither a field nor an id param — the read-only public APIs the
179
- // rule above was written for — still get no descriptor, so they still
180
- // get no id assertions.
181
101
  ment.id = { name: 'id', field: 'id', ...composite };
182
102
  }
183
103
  msg += ment.name + ' ';
@@ -185,14 +105,6 @@ const fieldTransform = async function (ctx) {
185
105
  return { ok: true, msg };
186
106
  };
187
107
  exports.fieldTransform = fieldTransform;
188
- // The separator that joins a composite id into one string.
189
- //
190
- // A forward slash cannot occur inside a single path segment — a raw `/`
191
- // would end the segment, and a value that legitimately contains one arrives
192
- // percent-encoded as `%2F` — so joining on it can never be ambiguous, and
193
- // splitting on it can never over-split. That is what makes the composite id
194
- // safe to carry as a single opaque string, which is the property the SDK and
195
- // Seneca entities are built on.
196
108
  const ID_SEP = '/';
197
109
  // Subfields that conventionally carry the identifying value of a nested
198
110
  // object, in preference order. github's repo `owner` is a user object whose
@@ -203,36 +115,6 @@ const NESTED_ID_KEYS = ['login', 'slug', 'name', 'key', 'id'];
203
115
  // The ops that address ONE record, most authoritative first. Only a
204
116
  // tie-break: identityParams compares candidates from all of them.
205
117
  const ID_OPS = ['load', 'update', 'patch', 'remove'];
206
- // The parameters that TOGETHER name one record: the trailing run of
207
- // ADJACENT variable segments on the addressing route.
208
- //
209
- // ADJACENCY IS THE WHOLE TEST, and it is what separates a compound key from
210
- // ordinary parent/child nesting:
211
- //
212
- // /repos/{owner}/{repo} -> owner, repo COMPOSITE
213
- // /api/planet/{planet_id}/moon/{moon_id} -> moon_id single
214
- // /repos/{owner}/{repo}/pulls/{pull_number} -> pull_number single
215
- //
216
- // A literal segment between two variables names a SUB-COLLECTION, so the
217
- // earlier variable scopes the later one — `planet_id` says which planet's
218
- // moons, and `moon_id` alone identifies the moon. Two variables with nothing
219
- // between them address no sub-collection: neither value names anything on
220
- // its own, and only the pair identifies a repository.
221
- //
222
- // Taking every variable on the path instead was tried first and is wrong on
223
- // most real specs — it made `moon` (planet_id + moon_id), petstore's `order`,
224
- // `pet` and `user`, and taxonomy's `domain` and `kingdom` all falsely
225
- // composite, which the apidef-validate goldens caught immediately. Nested
226
- // resources are the common shape; compound keys are the exception, and
227
- // adjacency is the thing that actually distinguishes them.
228
- //
229
- // Read from the op that names a single record, never from `list`: a
230
- // collection route's path params are the entity's parents. A point ending in
231
- // a literal is a verb ON the record (`.../{number}/merge`) and carries the
232
- // same variables, so it is a fallback rather than a different answer.
233
- // Walk back from a point's end, collecting variables until a literal stops
234
- // the run. That literal is the sub-collection boundary; anything before it
235
- // scopes this record rather than naming it.
236
118
  function trailingVars(point) {
237
119
  const segs = (point?.segments || []).filter((s) => null != s);
238
120
  const run = [];
@@ -245,15 +127,6 @@ function trailingVars(point) {
245
127
  return run;
246
128
  }
247
129
  function identityParams(ment) {
248
- // EVERY ID-BEARING OP AT ONCE, not the first one that offers a candidate.
249
- //
250
- // These four ops all address a single record, so all four describe the
251
- // same identity — but they do not all carry the same routes. gitlab's
252
- // `project` has `/api/v4/projects/{id}` under `remove` alone, while its
253
- // `load` carries only sub-resources like
254
- // `/api/v4/projects/{id}/uploads/{secret}/{filename}`. Returning on the
255
- // first op with any candidate therefore made a PROJECT identified by
256
- // `secret/filename`. The op order is now only a tie-break.
257
130
  const cands = [];
258
131
  for (let o = 0; o < ID_OPS.length; o++) {
259
132
  const mop = ment.op?.[ID_OPS[o]];
@@ -273,56 +146,12 @@ function identityParams(ment) {
273
146
  run,
274
147
  // Segments BEFORE the run: how much parent scope the route needs.
275
148
  scope: ((pt.segments || []).length - run.length),
276
- // DOES THE RUN END IN THE RECORD'S OWN KEY? Then it is the
277
- // record's address and nothing further is needed.
278
- //
279
- // This transform RENAMES that parameter to `id`, so a run ending in
280
- // it is this port's own statement of what identifies the record —
281
- // and the composite inference must not contradict it.
282
- // `/gists/{gist_id}` becomes `/gists/{id}` and is a gist;
283
- // `/gists/{gist_id}/{sha}` is a REVISION of one, and won on key
284
- // length alone, so a gist came out keyed `gist_id/sha` while the
285
- // generated SDK's own load match takes the single parameter. The
286
- // same contradiction gave cloudsmith's repo and vulnerability
287
- // compound keys their SDKs never address them by.
288
- //
289
- // Deliberately narrow: exactly `id` or an unrenamed `<entity>_id`,
290
- // never any `*_id`. `actor_type/actor_id` IS a compound key, and a
291
- // looser test breaks it.
292
149
  own: 'id' === run[run.length - 1] ||
293
150
  ment.name + '_id' === run[run.length - 1],
294
151
  order: o,
295
152
  });
296
153
  }
297
154
  }
298
- // WHICH ROUTE IS THE RECORD'S OWN ADDRESS.
299
- //
300
- // An entity gathers every route that reads it, and in a large
301
- // specification most of those are sub-resources. Three earlier rules were
302
- // measured against the validation corpus, and each is wrong:
303
- //
304
- // The FIRST route listed gave github's `repo` the single part
305
- // `subject_digest`, from
306
- // `/repos/{owner}/{repo}/attestations/{subject_digest}` — no compound
307
- // key at all, for the entity this feature exists for. Invisible on a
308
- // small spec, where the first item route IS the record's own.
309
- //
310
- // The SHORTEST route ending in a variable took cloudsmith's
311
- // `/vulnerabilities/{owner}/` — a LIST of an owner's vulnerabilities —
312
- // and cut a four-part key down to `owner`, dropping three more
313
- // composites. Ending in a variable does not make a route an address.
314
- //
315
- // The LONGEST trailing run took
316
- // `/orgs/{org}/teams/{team_slug}/repos/{owner}/{repo}` and made a TEAM
317
- // identified by `owner/repo`. A deep sub-resource can carry more
318
- // adjacent variables than the record's own route does.
319
- //
320
- // What separates them is PARENT SCOPE: the record's own route is the
321
- // least-qualified one that names it, and among equally-qualified routes
322
- // the one carrying the fullest key. `/repos/{owner}/{repo}` is qualified
323
- // by one segment and the attestations route by four; `/teams/{team_id}`
324
- // by one and the org-team-repo route by five; cloudsmith's vulnerability
325
- // routes are all qualified by one, so the fullest of them wins.
326
155
  const best = cands.reduce((b, c) => {
327
156
  if (null == b) {
328
157
  return c;
@@ -340,34 +169,6 @@ function identityParams(ment) {
340
169
  }, null);
341
170
  return null == best ? [] : best.run;
342
171
  }
343
- // THE PROPERTY MAPS A RESPONSE COULD BE DESCRIBING, best first.
344
- //
345
- // BOTH SPEC DIALECTS. An OpenAPI 3 response carries its schema under
346
- // `content['application/json']`; a SWAGGER 2 response carries it directly as
347
- // `schema`. Reading only the first resolved nothing for every Swagger 2 spec
348
- // in the validation corpus.
349
- //
350
- // JSON ONLY, where there is a choice. An operation may declare several media
351
- // types with different schemas, and field extraction uses the JSON one — so
352
- // picking whichever came first in source order could infer a path from an XML
353
- // or binary schema that the actual JSON record does not have.
354
- //
355
- // `allOf` IS EXPANDED, because a response that composes its entity that way
356
- // has neither `properties` nor `items` of its own. field extraction expands
357
- // it; not doing so here meant the fields were present while the id could not
358
- // be reconstructed.
359
- //
360
- // ONLY THE ENVELOPE IS DESCENDED, via the same `envelopeProp` rule field
361
- // extraction uses. Descending every object-valued property instead treats an
362
- // ordinary nested object as a whole record: for `{ slug, metadata: { tenant } }`
363
- // addressed by `{tenant}/{slug}`, `tenant` resolved to `tenant` rather than
364
- // `metadata.tenant` — a confidently wrong path, which is worse than no
365
- // mapping at all.
366
- //
367
- // ACTION POINTS ARE SKIPPED, as `identityParams` skips them: an action's
368
- // response is a verb's result, not a representation of the entity, so a field
369
- // that happens to appear there says nothing about what a returned record
370
- // carries.
371
172
  function responseCandidates(ment, def) {
372
173
  const out = [];
373
174
  const seen = new Set();
@@ -435,18 +236,12 @@ function responseCandidates(ment, def) {
435
236
  add(content[ctype]?.schema, opname);
436
237
  }
437
238
  }
438
- // Swagger 2 puts it here.
439
239
  add(resdef.schema, opname);
440
240
  }
441
241
  }
442
242
  }
443
243
  return out;
444
244
  }
445
- // Does this schema name the entity's own component?
446
- //
447
- // The comparison is on the CANONICALISED component name, the same function
448
- // the guide used to derive an entity name from a component in the first
449
- // place, so `Installment` and the entity `installment` meet.
450
245
  function namesEntity(schema, ment) {
451
246
  const xref = schema?.['x-ref'];
452
247
  if ('string' !== typeof xref) {
@@ -509,12 +304,6 @@ function partAliases(ment, part) {
509
304
  });
510
305
  return [...names];
511
306
  }
512
- // Where one part is carried in a given property map, or null.
513
- //
514
- // The four rules, in order, each a fact the spec states: a scalar property of
515
- // that name; the part naming this entity, resolved to `name`; a scalar
516
- // `<part>_name` / `_login` / `_slug`; or an object property's conventional
517
- // identifying subfield.
518
307
  function resolvePart(ment, part, aliases, props, def) {
519
308
  if (null == props) {
520
309
  return null;
@@ -548,47 +337,10 @@ function resolvePart(ment, part, aliases, props, def) {
548
337
  }
549
338
  return null;
550
339
  }
551
- // WHERE EACH COMPOSITE PART'S VALUE LIVES IN A RESPONSE.
552
- //
553
- // The parts are PATH PARAMETER names; a response names its fields whatever it
554
- // likes. Resolving one to the other is what lets an SDK put an id on a record
555
- // the API returned, rather than only address a record whose id it was given.
556
- //
557
- // The rules, in order, and each of them is a fact about the spec rather than
558
- // a guess:
559
- //
560
- // 1. a scalar field of exactly that name -> itself
561
- // 2. the part names this entity, and there is a `name` -> `name`
562
- // (`/repos/{owner}/{repo}` on entity `repo`, whose response calls the
563
- // repository `name`)
564
- // 3. a scalar `<part>_name` / `<part>_login` / `<part>_slug`
565
- // 4. an OBJECT field of that name -> `<part>.<conventional key>`
566
- // (`owner` is a user object; the value is `owner.login`)
567
- //
568
- // A part none of these resolve is left OUT. Downstream then knows the id
569
- // cannot be rebuilt for that entity and can say so, which is better than a
570
- // confidently wrong id on a real record. guide.aon can state it instead.
571
340
  function identityFrom(ment, parts, def) {
572
- // THE RESPONSE SCHEMA IS THE AUTHORITY, not `ment.fields`.
573
- //
574
- // `ment.fields` is merged across load, create, update and list, so a part
575
- // that exists only in a REQUEST BODY appears there too. Resolving against
576
- // it recorded such a part in `from` as though a returned record carried it,
577
- // and a consumer then rebuilt an id from a property the response never
578
- // sends — worse than leaving the part unresolved, which at least says so.
579
- //
580
- // Candidate property maps, in order: the response's own properties, then
581
- // one level into an envelope. A response that wraps the record
582
- // (`{ item: {...} }`, `{ data: [ {...} ] }`) states the record's fields one
583
- // level in, and searching only the wrapper found nothing.
584
341
  const candidates = responseCandidates(ment, def);
585
342
  const out = {};
586
343
  for (const part of parts) {
587
- // THE WIRE NAME AS WELL AS THE MODEL NAME. `identityParams` reads the
588
- // RENAMED parameter off the path segments, while a response keeps its own
589
- // casing — so a `tenantKey` renamed to `tenant_key` was looked up under a
590
- // name the response does not use, and the mapping was dropped for every
591
- // camel-cased or depluralized parameter.
592
344
  const aliases = partAliases(ment, part);
593
345
  let found = null;
594
346
  for (const props of candidates) {
@@ -608,15 +360,6 @@ function identityFrom(ment, parts, def) {
608
360
  function scalarStringField(f) {
609
361
  return String(f?.type || '').toUpperCase().includes('STRING');
610
362
  }
611
- // WHICH PARAMETER IS THE RECORD'S OWN KEY, among several that looked
612
- // adjacent. The same shape apidef's id handling recognises everywhere else:
613
- //
614
- // 1. one named exactly `id`
615
- // 2. `<entity>_id` — the entity's own id, however the path spells it
616
- // 3. any `*_id` — an id by name
617
- // 4. failing all that, the terminal parameter
618
- //
619
- // Position is the LAST resort, not the first.
620
363
  function singleKeyOf(ment, parts) {
621
364
  if (0 === parts.length) {
622
365
  return undefined;
@@ -626,12 +369,6 @@ function singleKeyOf(ment, parts) {
626
369
  ?? parts.find((p) => p.endsWith('_id'))
627
370
  ?? parts[parts.length - 1];
628
371
  }
629
- // The composite half of the id descriptor, or `{}` for the ordinary case.
630
- //
631
- // Emitted ONLY for a genuinely composite id (two or more addressing
632
- // parameters). A single-parameter entity already round-trips through one
633
- // `id` and gains nothing from carrying a one-element `parts`, so its
634
- // descriptor is left exactly as it was — no existing model output moves.
635
372
  function compositeId(ment, gent, def) {
636
373
  const gid = gent?.id;
637
374
  const sep = null != gid?.sep && '' !== String(gid.sep) ? String(gid.sep) : ID_SEP;
@@ -639,17 +376,6 @@ function compositeId(ment, gent, def) {
639
376
  // empty `parts`, because aontu resolves an empty list to nothing and the
640
377
  // key would arrive absent — indistinguishable from never having been set.
641
378
  if (null != gid && false === gid.composite) {
642
- // DISABLING COMPOSITE MUST NOT DISABLE THE ID. The correction says these
643
- // adjacent parameters are not a compound key; it does not say the record
644
- // has no key. Returning a bare `{}` left an entity whose response has no
645
- // literal `id` with no descriptor at all — the false positive removed and
646
- // nothing identifying the real key.
647
- //
648
- // WHICH of the adjacent parameters is that key is decided by the same
649
- // id-finding rules apidef uses elsewhere, not by position. Taking the
650
- // terminal one picked `archive_format` for
651
- // `/artifacts/{artifact_id}/{archive_format}` — the modifier, precisely
652
- // the false positive the correction exists to undo.
653
379
  return { single: singleKeyOf(ment, identityParams(ment)) };
654
380
  }
655
381
  // `from` STATED IN guide.aon WINS PER PART, so a spec can correct one
@@ -709,19 +435,6 @@ function resolveOpFields(ment, mop, mpoint, def) {
709
435
  req: !!fielddef.required,
710
436
  op: {},
711
437
  };
712
- // Carry the spec's own words for the field, when it has any.
713
- //
714
- // Every generated per-entity table has a Description column and every cell
715
- // was blank, because nothing ever read the property `description` the spec
716
- // supplies. Trimmed, and only when it is a non-empty string: a whitespace
717
- // or non-string value would put a meaningless cell where an empty one is
718
- // honest.
719
- // ONE LINE, not the whole description. Every generated Readme drops this
720
- // straight into a markdown table cell, where a raw newline ends the row
721
- // and orphans the rest of the table — and specs put bullet lists, fenced
722
- // examples and multi-paragraph notes in `description`. firstSentence is
723
- // the same reduction the API summary uses, so `short` means the same
724
- // thing wherever it appears.
725
438
  const fdesc = fielddef.description;
726
439
  if ('string' === typeof fdesc && '' !== fdesc.trim()) {
727
440
  const short = (0, utility_1.firstSentence)(fdesc);
@@ -729,20 +442,6 @@ function resolveOpFields(ment, mop, mpoint, def) {
729
442
  mfield.short = short;
730
443
  }
731
444
  }
732
- // SPEC FACTS ABOUT THE FIELD, carried through verbatim.
733
- //
734
- // These four are declared by OpenAPI on the property and were being
735
- // dropped on the floor. `readOnly` is the one that matters most: it is
736
- // the difference between a field a client MAY send and one it may not,
737
- // and nothing else in the model says which — so every generator has been
738
- // putting server-assigned fields into the type a caller fills in.
739
- //
740
- // ONLY WHEN THE SPEC SAYS SO, and for the booleans only when TRUE. Each
741
- // defaults to false in OpenAPI, so an absent key and an explicit `false`
742
- // carry the same information; emitting the false ones would add a key to
743
- // every field of every model and say nothing. Same discipline as
744
- // `short`: absent means "the spec did not say", never "apidef dropped
745
- // it".
746
445
  for (const flag of ['readOnly', 'writeOnly', 'deprecated']) {
747
446
  if (true === fielddef[flag]) {
748
447
  mfield[flag] = true;
@@ -780,7 +479,6 @@ function findGraphqlFieldDefs(ment, mpoint, def) {
780
479
  return [];
781
480
  }
782
481
  const out = [];
783
- // Sorted by construction in parse/graphql.ts, so output stays byte-stable.
784
482
  for (const fname of Object.keys(gtype.fields)) {
785
483
  const f = gtype.fields[fname];
786
484
  if (f.deprecated) {
@@ -799,17 +497,10 @@ function findGraphqlFieldDefs(ment, mpoint, def) {
799
497
  // custom scalars left unconstrained.
800
498
  type: 'ENUM' === kind ? 'string' : gqlFieldType(f.type),
801
499
  required: f.reqd,
802
- // GraphQL puts the field's own words on GqlField.desc (see
803
- // parse/graphql.ts). resolveOpFields reads `description`, the OpenAPI
804
- // spelling, so name it that here rather than teaching the reader two.
805
500
  description: f.desc,
806
501
  });
807
502
  }
808
503
  else if (('OBJECT' === kind || 'INTERFACE' === kind) && !f.list) {
809
- // To-one relation. The default fragment selects `team { id }`, so the
810
- // response carries a nested stub object — declare it as such. Naming a
811
- // flat `team_id` here would advertise a field the wire never returns,
812
- // since nothing flattens the response.
813
504
  const idField = ftype.fields?.id;
814
505
  if (null != idField) {
815
506
  out.push({
@@ -823,12 +514,6 @@ function findGraphqlFieldDefs(ment, mpoint, def) {
823
514
  }
824
515
  return out;
825
516
  }
826
- // GraphQL named type -> the type names the field typing understands.
827
- //
828
- // Built-ins only: a custom scalar (JSON, JSONObject, Upload, ...) can hold
829
- // any JSON value, so advertising it as a string would misdescribe the data
830
- // and make generated validation reject values the schema accepts. Enums are
831
- // mapped by the caller, which knows they are strings.
832
517
  function gqlFieldType(typeName) {
833
518
  return 'Int' === typeName ? 'integer' :
834
519
  'Float' === typeName ? 'number' :
@@ -845,7 +530,7 @@ function findFieldDefs(ment, mop, mpoint, def) {
845
530
  const fielddefs = [];
846
531
  const pathdef = def.paths[mpoint.orig];
847
532
  const method = mpoint.method.toLowerCase();
848
- const opdef = pathdef[method];
533
+ const opdef = pathdef?.[method];
849
534
  if (opdef) {
850
535
  const responses = opdef.responses;
851
536
  const requestBody = opdef.requestBody;
@@ -854,13 +539,6 @@ function findFieldDefs(ment, mop, mpoint, def) {
854
539
  fieldSets = (0, jostraca_1.getx)(responses, '200 content "application/json" schema') ??
855
540
  (0, jostraca_1.getx)(responses, '200 schema');
856
541
  if ('list' == mop.name) {
857
- // List responses commonly come in three shapes:
858
- // 1. direct array — { type: array, items: { ...item } }
859
- // 2. wrapper object — { properties: { items: [Item], page, ... } }
860
- // (a single array-of-object property inside an object schema)
861
- // 3. legacy "list of created items" under 201
862
- // Resolve to the inner item schema when we can identify one
863
- // unambiguously; otherwise fall through to the 200 schema as-is.
864
542
  const unwrapped = unwrapArrayWrapper(fieldSets);
865
543
  if (unwrapped) {
866
544
  fieldSets = unwrapped;
@@ -876,14 +554,6 @@ function findFieldDefs(ment, mop, mpoint, def) {
876
554
  fieldSets = (0, jostraca_1.getx)(responses, '201 content "application/json" schema') ??
877
555
  (0, jostraca_1.getx)(responses, '201 schema');
878
556
  }
879
- // Single-entity responses get the same treatment the list branch above
880
- // already gives collections: a body that is only an envelope around the
881
- // entity — `{item: {...}}` — describes the WRAPPER, not the entity, so
882
- // its sole property would otherwise be harvested as a field. That is
883
- // how an entity `todoitem` ended up with a required `item` field of
884
- // type object, which then appeared in the generated create/update data
885
- // types. envelopeProp applies the same two rules used to pick the
886
- // response transform, so the field list and the transform agree.
887
557
  if ('list' != mop.name) {
888
558
  const envelope = (0, utility_1.envelopeProp)(fieldSets?.properties, mop.name);
889
559
  if (null != envelope) {
@@ -891,15 +561,6 @@ function findFieldDefs(ment, mop, mpoint, def) {
891
561
  }
892
562
  }
893
563
  }
894
- // AN ACTION'S RESPONSE IS THE VERB'S RESULT -- unless it is the entity.
895
- //
896
- // `POST /pet/{petId}/uploadImage` answers with an `ApiResponse`, which
897
- // says how the upload went; `GET /v2/installments/active` answers with
898
- // `[Installment]`, which is what an installment IS. Both are actions, so
899
- // the verb alone cannot tell them apart -- but the component can, and
900
- // parse.ts keeps it: every resolved `$ref` leaves its original pointer
901
- // behind as `x-ref`, so the component a response was written against
902
- // survives inlining and can be compared with the entity that owns it.
903
564
  if (isAction && !namesEntity(fieldSets, ment)) {
904
565
  return fielddefs;
905
566
  }
@@ -996,7 +657,6 @@ function findExampleObject(opdef) {
996
657
  example = (0, jostraca_1.getx)(resdef, 'examples "application/json"');
997
658
  if (null != example && 'object' === typeof example)
998
659
  return unwrapExample(example);
999
- // Swagger 2.0: schema.example
1000
660
  example = (0, jostraca_1.getx)(resdef, 'schema example');
1001
661
  if (null != example && 'object' === typeof example)
1002
662
  return unwrapExample(example);
@@ -1009,16 +669,6 @@ function unwrapExample(example) {
1009
669
  }
1010
670
  return example;
1011
671
  }
1012
- // unwrapArrayWrapper inspects a list-response schema and, when it is an
1013
- // object with a single array-of-object-schema property (e.g.
1014
- // { boards: [Board] }, { items: [Foo], page, total, ... }), returns the
1015
- // inner item schema so that field resolution sees the actual entity
1016
- // properties rather than the wrapper's bookkeeping.
1017
- //
1018
- // Returns null if the input is not unambiguously such a wrapper:
1019
- // - schema is already an array → return null (let caller use it directly)
1020
- // - no array-of-object-schema property → return null
1021
- // - more than one array-of-object-schema property → ambiguous, return null
1022
672
  function unwrapArrayWrapper(schema) {
1023
673
  if (null == schema || 'object' !== typeof schema)
1024
674
  return null;
@@ -1073,26 +723,9 @@ function mergeField(mop, existingField, newField) {
1073
723
  type: newField.type,
1074
724
  };
1075
725
  }
1076
- // Field identity is first-writer-wins, but a DESCRIPTION is not part of
1077
- // identity: the op that first names a field is often not the one that
1078
- // documents it (a load response referencing a bare component, a create body
1079
- // referencing the annotated one). Take the first non-empty description in
1080
- // opFieldPrecedence order and keep it — dropping it left a blank cell in
1081
- // every generated table while the spec had the words all along.
1082
726
  if (null == existingField.short && null != newField.short) {
1083
727
  existingField.short = newField.short;
1084
728
  }
1085
- // The spec facts merge the same way, and for the same reason: one schema
1086
- // annotates the field and another references it bare, so taking the first
1087
- // declaration in opFieldPrecedence order is what finds the annotation.
1088
- //
1089
- // THE PRECEDENCE ORDER PUTS `load` FIRST, WHICH IS THE SAFE DIRECTION HERE.
1090
- // A field the response schema marks readOnly and a request body also lists
1091
- // is a self-contradictory spec — OpenAPI says a client must not send a
1092
- // readOnly property at all — and this resolves it by believing the
1093
- // restriction rather than the omission. Marking a writable field readOnly
1094
- // costs a caller one field; the other way round sends a value the server
1095
- // rejects.
1096
729
  for (const flag of ['readOnly', 'writeOnly', 'deprecated', 'format']) {
1097
730
  if (null == existingField[flag] && null != newField[flag]) {
1098
731
  existingField[flag] = newField[flag];