@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
package/dist/utility.js CHANGED
@@ -33,6 +33,7 @@ exports.inferFieldType = inferFieldType;
33
33
  exports.normalizeFieldName = normalizeFieldName;
34
34
  exports.prefixLeadingDigit = prefixLeadingDigit;
35
35
  exports.debugpath = debugpath;
36
+ exports.debugpathOn = debugpathOn;
36
37
  exports.findPathsWithPrefix = findPathsWithPrefix;
37
38
  exports.writeFileSyncWarn = writeFileSyncWarn;
38
39
  exports.warnOnError = warnOnError;
@@ -115,35 +116,6 @@ function formatJsonSrc(jsonsrc) {
115
116
  .replace(RE_JSON_TRAILING_BRACE, '}\n')
116
117
  .replace(RE_JSON_COMMENT, '\n\n$1# $2 $3');
117
118
  }
118
- // Common irregular plurals, in the form plural → singular. Used at the
119
- // head of depluralize() to short-circuit cases the suffix rules below
120
- // would otherwise mishandle.
121
- //
122
- // Three over-strip classes are worked around here because the surface
123
- // form gives no clean discriminator:
124
- //
125
- // * `-Vse+s` plurals (houses, phases, noses, …) would hit the
126
- // generic `-ses → ∅` rule and become hous/phas/nos. Every such
127
- // -se+s plural needs an explicit entry.
128
- //
129
- // * `-che+s` plurals (caches, niches, headaches, …) would hit the
130
- // generic `-ches → ∅` rule and become cach/nich/headach. Same
131
- // pattern: no letter-doubling tell exists (cache vs church both
132
- // have a single 'ch'), so each -che singular is enumerated.
133
- //
134
- // * `-oe+s` plurals (shoes, canoes, oboes) would hit the generic
135
- // `-oes → -o` rule (for potatoes/heroes) and become sho/cano/obo.
136
- // Only collision-safe entries are listed: a key must not also be a
137
- // suffix of a real `-o`+es plural (e.g. `toes` is excluded because
138
- // it would turn tomatoes → tomatoe).
139
- //
140
- // Keys are lowercase; depluralize() does a case-insensitive lookup
141
- // and reapplies the caller's casing on the way out.
142
- //
143
- // Null-prototype: these tables are indexed by spec-derived names, so a
144
- // schema or path segment called `constructor` / `__proto__` / `toString`
145
- // would otherwise resolve to the inherited Object member and be returned
146
- // as a "match" — crashing matchCase() on a function. See NULL_PROTO_NOTE.
147
119
  const IRREGULARS = Object.assign(Object.create(null), {
148
120
  'analytics': 'analytics',
149
121
  'analyses': 'analysis',
@@ -209,26 +181,11 @@ const IRREGULARS = Object.assign(Object.create(null), {
209
181
  'women': 'woman',
210
182
  'yes': 'yes',
211
183
  });
212
- // NULL_PROTO_NOTE: every plain-object lookup table in this module whose keys
213
- // come from an API spec is built with a null prototype
214
- // (`Object.assign(Object.create(null), {...})`). Without it, `TABLE[name]`
215
- // inherits from Object.prototype, so `TABLE['constructor']` yields the Object
216
- // constructor (truthy, a function) and `TABLE['__proto__']` yields
217
- // Object.prototype (truthy, an object). Both then flow into code expecting a
218
- // string. A spec with `components.schemas.Constructor` is enough to reach
219
- // this: canonize -> depluralize -> matchCase -> `.toLowerCase is not a
220
- // function`, failing the whole build at the guide stage. Keep new tables
221
- // null-prototype, or use a Map (CANONIZE_CACHE already does).
222
184
  // Sorted longest-first so the most specific IRREGULARS suffix wins.
223
185
  // Without this, 'women' would be shadowed by 'men' (3 < 5) under
224
186
  // insertion-order iteration. Both happen to round-trip correctly
225
187
  // today, but the sort makes any future entry safe by construction.
226
188
  const IRREGULAR_KEYS = Object.keys(IRREGULARS).sort((a, b) => b.length - a.length);
227
- // Reapply the case pattern of `source` to `target`. Used so the
228
- // case-insensitive lookups in depluralize() preserve the caller's
229
- // casing on the way out (HOUSES → HOUSE, Houses → House, houses →
230
- // house). Falls through to `target` unchanged for mixed-case sources
231
- // that don't fit one of the three canonical patterns.
232
189
  function matchCase(source, target) {
233
190
  if (source === source.toLowerCase())
234
191
  return target.toLowerCase();
@@ -239,20 +196,6 @@ function matchCase(source, target) {
239
196
  }
240
197
  return target;
241
198
  }
242
- // Per-model plural overrides, populated from the model's
243
- // `main.custom.plurals` section at apidef pipeline entry and cleared
244
- // between runs. Checked by depluralize() before the built-in
245
- // IRREGULARS table and rule chain — so a model can override any
246
- // default depluralization, including correct-by-default cases, when
247
- // its domain demands a different singular (e.g. fitness API with
248
- // {axes: axe}, photography app with {lenses: lens}).
249
- //
250
- // Module-level rather than a parameter so the many existing
251
- // depluralize/canonize call sites across transforms and guide
252
- // inherit the override without signature churn. apidef is
253
- // single-model-per-process; if that ever changes, switch this to a
254
- // per-context map.
255
- // Null-prototype: see NULL_PROTO_NOTE above.
256
199
  let CUSTOM_PLURALS = Object.create(null);
257
200
  let CUSTOM_PLURAL_KEYS = [];
258
201
  function setCustomPlurals(plurals) {
@@ -313,7 +256,6 @@ function depluralize(word) {
313
256
  // Rules for regular plurals (applied in order). The -ies and -ves
314
257
  // rules add a letter, so they need to match the case of the dropped
315
258
  // suffix; all other rules just slice and inherit the caller's case.
316
- // -ies -> -y (cities -> city), but only if result is > 2 chars
317
259
  if (lower.endsWith('ies') && word.length > 3) {
318
260
  const dropped = word.slice(-3);
319
261
  const y = dropped === dropped.toUpperCase() ? 'Y' : 'y';
@@ -342,12 +284,6 @@ function depluralize(word) {
342
284
  if (lower.endsWith('nses')) {
343
285
  return word.slice(0, -1);
344
286
  }
345
- // -zes plurals come from -ze singulars (prize, size, freeze, maze,
346
- // breeze, …) far more often than from a bare -z taking -es. The only
347
- // -zes plurals that strip the full -es have a doubled-z stem
348
- // (buzz/buzzes, fez/fezzes). Discriminate on -zzes so prizes → prize
349
- // instead of priz. Mirrors the -ses/-Vse+s problem the IRREGULARS
350
- // table works around for the -se case.
351
287
  if (lower.endsWith('zzes')) {
352
288
  return word.slice(0, -2);
353
289
  }
@@ -359,7 +295,6 @@ function depluralize(word) {
359
295
  lower.endsWith('shes') || lower.endsWith('ches')) {
360
296
  return word.slice(0, -2);
361
297
  }
362
- // -s -> remove -s (cats -> cat), but only if result is > 2 chars
363
298
  if (lower.endsWith('s') &&
364
299
  !lower.endsWith('ss') &&
365
300
  !lower.endsWith('us') &&
@@ -371,9 +306,13 @@ function depluralize(word) {
371
306
  }
372
307
  function find(obj, qkey) {
373
308
  const vals = [];
309
+ const seen = new WeakSet();
374
310
  const collect = (o) => {
375
311
  if (!o || 'object' !== typeof o)
376
312
  return;
313
+ if (seen.has(o))
314
+ return;
315
+ seen.add(o);
377
316
  if (Array.isArray(o)) {
378
317
  for (let i = 0; i < o.length; i++)
379
318
  collect(o[i]);
@@ -456,7 +395,6 @@ function $APPEND(inj, val, ref, store) {
456
395
  const dval = dparent?.[key];
457
396
  const vstore = { ...store };
458
397
  vstore.$TOP = { [key]: dval };
459
- // const ptval = transform({ [key]: dval }, { [key]: xform }, {
460
398
  const ptval = (0, struct_1.inject)({ [key]: xform }, vstore, {
461
399
  meta: { ...inj.meta },
462
400
  errs: inj.errs,
@@ -500,7 +438,6 @@ function $SELECT(inj, _val, _ref, store) {
500
438
  '$KEY': { '`$LIKE`': selector.toString() }
501
439
  };
502
440
  }
503
- // TODO: select should be safe for scalars
504
441
  const children = (0, struct_1.select)(dparents, selector);
505
442
  if (0 < children.length) {
506
443
  for (let child of children) {
@@ -509,8 +446,6 @@ function $SELECT(inj, _val, _ref, store) {
509
446
  (0, struct_1.inject)((0, struct_1.clone)({ [child.$KEY]: descendor }), vstore, {
510
447
  meta: (0, struct_1.merge)([
511
448
  inj.meta,
512
- // TODO: need this hack as struct does not provide a way to get grandparent keys
513
- // also, these capture actions are not preserving the path!
514
449
  { select: { key: { [(0, struct_1.slice)(inj.path, 1, -1).join('+')]: child.$KEY } } }
515
450
  ]),
516
451
  errs: inj.errs,
@@ -524,8 +459,6 @@ function $RECASE(inj, val, ref, store) {
524
459
  && null != inj.prior
525
460
  && null != inj.prior.prior) {
526
461
  const dval = inj.parent[inj.key];
527
- // TODO: handle paths more generally! use inj.prior?
528
- // TODO: mkae this into a utility method on inj?
529
462
  const dkey = inj.prior.key;
530
463
  const gkey = inj.prior.prior.key;
531
464
  const vstore = { ...store };
@@ -542,13 +475,6 @@ function $RECASE(inj, val, ref, store) {
542
475
  inj.setval(tval, 2);
543
476
  }
544
477
  }
545
- // A special-purpose regex-style matcher for url paths.
546
- // t - text part
547
- // p - param part
548
- // / - part separator
549
- // / at start - must match from start
550
- // / at end - must match to end
551
- // See utility.test.ts for examples
552
478
  function pathMatch(path, expr) {
553
479
  if (null == path) {
554
480
  return null;
@@ -658,9 +584,6 @@ function formatJSONIC(val, opts) {
658
584
  '`' + JSON.stringify(v)
659
585
  .substring(1)
660
586
  .replace(/\\n/g, '\n')
661
- // Inside a JSONIC backtick literal a double quote is a literal
662
- // character, so unescape JSON's \" back to " (was previously
663
- // replaced with ':' which silently corrupted quoted text).
664
587
  .replace(/\\"/g, '"')
665
588
  .replace(/`/g, '\\`')
666
589
  .replace(/"$/, '`'));
@@ -690,13 +613,7 @@ function formatJSONIC(val, opts) {
690
613
  };
691
614
  return renderJSONIC(val, hsepd, showd, useColor, maxlines, exclude, c, renderPrimitive, renderComment, false);
692
615
  }
693
- function renderJSONIC(val, hsepd, showd, useColor, maxlines, exclude, c, renderPrimitive, renderComment,
694
- // True once the value has already been through decircular. `seen` never
695
- // forgets a node, so a merely REPEATED reference (a shared node in a DAG,
696
- // not a cycle) also lands in the fallback below. If decircular leaves such
697
- // a repeat in place, retrying forever overflows the stack — which is what
698
- // formatting a deep validation error used to do. Retry at most once.
699
- decircularized) {
616
+ function renderJSONIC(val, hsepd, showd, useColor, maxlines, exclude, c, renderPrimitive, renderComment, decircularized) {
700
617
  const space = ' ';
701
618
  const seen = new WeakSet();
702
619
  let stack = new Array(32);
@@ -817,16 +734,6 @@ decircularized) {
817
734
  }
818
735
  return lines.join('\n') + '\n';
819
736
  }
820
- // Canonical type-sentinel vocabulary. VALID_CANON maps an OpenAPI type NAME
821
- // to its `$SENTINEL` form; CANON_ONE is the union sentinel produced by
822
- // `validator` for a multi-type (`['`$ONE`', [member, ...]]`). Both are part
823
- // of the public API so downstream consumers (e.g. @voxgig/sdkgen's
824
- // sentinel -> language-type table) can verify they cover the full set
825
- // instead of hand-syncing against this file.
826
- //
827
- // Null-prototype (see NULL_PROTO_NOTE): `type` values come from the spec, so
828
- // a schema declaring `type: constructor` would otherwise return the Object
829
- // constructor here rather than falling through to the 'Any' default.
830
737
  const VALID_CANON = Object.assign(Object.create(null), {
831
738
  'string': '`$STRING`',
832
739
  'number': '`$NUMBER`',
@@ -858,61 +765,24 @@ function transliterate(s) {
858
765
  return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
859
766
  }
860
767
  const CANONIZE_CACHE = new Map();
768
+ // A trailing plural after an acronym is part of the word, and snakify would
769
+ // split it letter by letter. See docs/design/derived-names.md
770
+ const ACRONYM_PLURAL_RE = /([A-Z]{2,})s(?![a-zA-Z])/g;
861
771
  function canonize(s) {
862
772
  if (null == s || '' === s)
863
773
  return '';
864
774
  const cached = CANONIZE_CACHE.get(s);
865
775
  if (undefined !== cached)
866
776
  return cached;
867
- const out = depluralize((0, jostraca_1.snakify)(transliterate(s).replace(FILE_EXT_RE, '')))
777
+ const deacronymed = transliterate(s)
778
+ .replace(FILE_EXT_RE, '')
779
+ .replace(ACRONYM_PLURAL_RE, (_m, run) => run[0] + run.slice(1).toLowerCase() + 's');
780
+ const out = depluralize((0, jostraca_1.snakify)(deacronymed))
868
781
  .replace(/[^a-zA-Z_0-9]/g, '');
869
782
  CANONIZE_CACHE.set(s, out);
870
783
  return out;
871
784
  }
872
- // Canonicalise a FIELD name — which is a WIRE identifier, not a type name.
873
- //
874
- // `canonize` is right for entity/type names: it snakifies and depluralizes so
875
- // `Users` and `user-items` converge on `user` / `user_item`. Applied to a
876
- // field it is actively WRONG, because the name has to match the JSON the
877
- // server actually sends:
878
- //
879
- // modelType -> canonize -> model_type (server sends modelType)
880
- // items -> canonize -> item (server sends items)
881
- //
882
- // Nothing maps back: the model's `alias.field` map is emitted empty and no
883
- // generator consumes it, so the wire name is simply lost. Across the fleet's
884
- // specs that renamed 23% of all fields (146 repos) and depluralized another
885
- // 13% — every one of those SDKs reading a key the server never sends.
886
- //
887
- // So: keep the transliteration and identifier sanitisation that stop a name
888
- // being unusable in a target language, and drop the snakify/depluralize that
889
- // change what the name MEANS. Case and plurality are preserved verbatim.
890
785
  const CANONIZE_FIELD_CACHE = new Map();
891
- // Re-split a boundary-less entity name using the component name's boundaries.
892
- //
893
- // ONLY when the name has no boundaries of its own. A name that already
894
- // contains `_` was split by something that knew where the words were, and is
895
- // left exactly as it is.
896
- //
897
- // ONLY on an EXACT concatenation match, which is what stops this inventing
898
- // anything. The component's segments are accumulated left to right and the
899
- // result is used only if it equals the path-derived name with its separators
900
- // removed:
901
- //
902
- // payeeverification + payee_verification_result
903
- // payee -> "payee" no
904
- // payee_verification -> "payeeverification" YES -> payee_verification
905
- //
906
- // virtual + payment_account
907
- // payment -> "payment" no
908
- // payment_account -> "paymentaccount" no -> unchanged
909
- //
910
- // So a component that has nothing to do with the path token cannot rename it,
911
- // and a component that merely EXTENDS the token (`payment_account_entry` for
912
- // `payment`) cannot either -- only the prefix that reconstructs the token
913
- // exactly is taken. A different entity name is never reachable from here; the
914
- // same letters in the same order, with separators restored, is the whole of
915
- // what this can produce.
916
786
  function resplitFromCmp(entname, cmp, why) {
917
787
  if (null == entname || '' === entname || entname.includes('_')) {
918
788
  return entname;
@@ -945,12 +815,6 @@ function canonizeField(s) {
945
815
  CANONIZE_FIELD_CACHE.set(s, out);
946
816
  return out;
947
817
  }
948
- // Namespace-qualified schema names (ASP.NET / Java style:
949
- // "NoFrixion.MoneyMoov.Models.PaymentRequests.MerchantPayment",
950
- // "com.example.api.Payment") describe the type by their LAST dotted
951
- // segment; the namespace prefix is packaging noise. Reduce to the last
952
- // meaningful segment — skipping version-ish ("v2", "10") or too-short
953
- // tails — so entity names derive from the type, not the namespace.
954
818
  function stripSchemaNamespace(name) {
955
819
  if (null == name || !name.includes('.'))
956
820
  return name;
@@ -971,24 +835,6 @@ function canonizeCmpName(orig) {
971
835
  return canonize(stripSchemaNamespace(orig));
972
836
  }
973
837
  const FIRST_LETTER_RE = /[a-zA-Z]/;
974
- // No target language permits an identifier that starts with a digit, so a
975
- // name derived from one — a `3dsSession` schema, a `/2fa` path segment, a
976
- // `_3DSecure` GraphQL type — is prefixed with an `n`.
977
- //
978
- // The prefix takes the case of the name it guards: lower for `3ds_session`,
979
- // upper for `3DSecure`. That keeps the result inside whatever casing
980
- // convention the caller was already working in, so a later PascalCase or
981
- // camelCase conversion has nothing to undo. A name with no letter in it at
982
- // all (`404`) takes the lower-case prefix.
983
- //
984
- // This is the ONE place the rule lives. Entity names reach it through
985
- // `ensureMinEntityName` and the GraphQL guide's `entityName`; project slugs
986
- // through `sanitizeSlug`.
987
- //
988
- // FIELD names deliberately do NOT come here. A field name is a WIRE
989
- // identifier and renaming it makes the SDK read a key the server never sends
990
- // — the mistake `canonizeField` exists to document. Targets escape those at
991
- // the point of emission instead.
992
838
  function prefixLeadingDigit(s) {
993
839
  if (null == s || '' === s)
994
840
  return s;
@@ -1096,13 +942,6 @@ function ensureMinEntityName(name, existing) {
1096
942
  padded = padded + padding;
1097
943
  }
1098
944
  if (padded !== name && null != existing[padded]) {
1099
- // The name was modified (truncated/sanitized) and collides with an
1100
- // existing entity. Only a collision between DIFFERENT origins needs a
1101
- // numeric suffix — the same original name re-encountered (e.g. the same
1102
- // long schema referenced by several methods on one path) must reuse the
1103
- // existing entity so its ops merge instead of minting phantom
1104
- // "<entity>2/3/4" entities. Entities record their pre-truncation name
1105
- // as `longname`; entries without one keep the old always-suffix rule.
1106
945
  if (existing[padded].longname === name) {
1107
946
  return padded;
1108
947
  }
@@ -1119,23 +958,7 @@ function ensureMinEntityName(name, existing) {
1119
958
  }
1120
959
  // Unconditional suffixes: framework noise, always stripped.
1121
960
  const CMP_SUFFIXES = ['_rest_controller', '_controller', '_response', '_request'];
1122
- // Guarded suffixes: pagination wrappers ('_page_response', '_page') and
1123
- // op-reply wrappers ('_create_response', '_update_response') fold wrapper
1124
- // schemas (BeneficiaryPageResponse, MerchantTokenPage,
1125
- // BeneficiariesCreateResponse, ...) into their base entity — but ONLY when
1126
- // the remainder is itself a known component schema (the wrapper
1127
- // convention). Without that guard a real noun gets mangled: an API whose
1128
- // resource IS a page (LandingPage entity at /landing-pages) must keep
1129
- // 'landing_page', not become 'landing'. Order matters: longer first, since
1130
- // '_page_response'/'_create_response' also end with '_response'. Bare
1131
- // '_create'/'_update' are never stripped: too likely part of a real noun.
1132
961
  const CMP_GUARDED_SUFFIXES = ['_create_response', '_update_response', '_page_response', '_page'];
1133
- // Op-reply wrappers of the form <namespace><Verb>Result (incident.io:
1134
- // AlertsResolveResultV2, SeveritiesListResultV1). Same idea as
1135
- // '_create_response' above, but the verb varies and sits before the wrapper
1136
- // word rather than being part of a fixed pair, so the shape is matched rather
1137
- // than enumerated. Guarded exactly like the others: nothing is stripped
1138
- // unless the remainder is a real schema.
1139
962
  const CMP_RESULT_VERBS = [
1140
963
  'list', 'create', 'show', 'update', 'delete', 'remove',
1141
964
  'get', 'edit', 'resolve', 'rotate', 'search',
@@ -1149,26 +972,6 @@ const CMP_PREFIXES = ['get_', 'post_', 'put_', 'delete_', 'patch_'];
1149
972
  function guideActive(node) {
1150
973
  return false !== node?.active;
1151
974
  }
1152
- // An API's ACCESS-TOKEN EXCHANGE is not a resource, and must not become an
1153
- // entity. The shape apidef looks for is the one every such endpoint has:
1154
- //
1155
- // 1. The spec as a whole is SECURED (a top-level `security` requirement).
1156
- // Without that, a per-operation `security: []` clears nothing and
1157
- // carries no signal at all.
1158
- // 2. The operation clears that requirement with its own `security: []` —
1159
- // it is the one call a client can make before it holds a credential,
1160
- // because it is what issues them.
1161
- // 3. It is a POST. A credential exchange writes; a GET that happens to
1162
- // return a field called `token` is far likelier to be a resource.
1163
- // 4. Its success response carries a TOKEN-shaped field.
1164
- //
1165
- // All four together, or it is a resource. There is deliberately no vendor
1166
- // extension and no overlay to say otherwise (ADR-002): a spec apidef does
1167
- // not control cannot be annotated anyway, and a heuristic that can be
1168
- // corrected in guide.aon needs no second correction surface.
1169
- //
1170
- // Returns the field names the exchange uses, which is what sdkgen's
1171
- // `secrets` feature needs to drive it, or null when this is a resource.
1172
975
  const AUTH_TOKEN_FIELDS = [
1173
976
  'access_token', 'accessToken', 'access-token',
1174
977
  'id_token', 'idToken',
@@ -1188,8 +991,6 @@ function authExchangeOp(op, specSecured) {
1188
991
  if (true !== specSecured) {
1189
992
  return null;
1190
993
  }
1191
- // An empty ARRAY, specifically. `security` absent means "inherit the
1192
- // global requirement"; `security: []` means "no credential needed here".
1193
994
  if (!Array.isArray(op?.security) || 0 !== op.security.length) {
1194
995
  return null;
1195
996
  }
@@ -1230,9 +1031,6 @@ function schemaProps(schema) {
1230
1031
  }
1231
1032
  return Object.keys(props);
1232
1033
  }
1233
- // First name in `names` that the schema declares, compared case-insensitively
1234
- // so `Access_Token` matches `access_token`. Ordered by the CANDIDATE list, not
1235
- // by declaration order, so `access_token` wins over a sibling `token`.
1236
1034
  function firstFieldMatch(props, names) {
1237
1035
  const lower = new Map();
1238
1036
  for (const p of props) {
@@ -1344,6 +1142,10 @@ function warnOnError(where, warn, fn, result) {
1344
1142
  return result;
1345
1143
  }
1346
1144
  }
1145
+ function debugpathOn() {
1146
+ const apipath = process.env.APIDEF_DEBUG_PATH;
1147
+ return null != apipath && '' !== apipath;
1148
+ }
1347
1149
  function debugpath(pathStr, methodName, ...args) {
1348
1150
  const apipath = process.env.APIDEF_DEBUG_PATH;
1349
1151
  if (null == apipath || '' === apipath)
@@ -1353,7 +1155,6 @@ function debugpath(pathStr, methodName, ...args) {
1353
1155
  // Check if path matches
1354
1156
  if (pathStr !== targetPath)
1355
1157
  return;
1356
- // If a method is specified in apipath and we have a method name, check if it matches
1357
1158
  if (targetMethod && methodName) {
1358
1159
  if (methodName.toLowerCase() !== targetMethod.toLowerCase())
1359
1160
  return;
@@ -1384,7 +1185,6 @@ function findPathsWithPrefix(ctx, pathStr, opts) {
1384
1185
  });
1385
1186
  return matchingPaths.length;
1386
1187
  }
1387
- // TODO: move to jostraca?
1388
1188
  function allcapify(s) {
1389
1189
  return 'string' === typeof s ? (0, jostraca_1.snakify)(s).toUpperCase() : '';
1390
1190
  }
@@ -1505,47 +1305,11 @@ function isEntityWrapperProp(propSchema) {
1505
1305
  const t = propSchema.type;
1506
1306
  return 'object' === t || 'array' === t;
1507
1307
  }
1508
- // A response body that is nothing but a single wrapper property is an
1509
- // ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
1510
- // `{items: [...]}`, `{results: [...]}`. Return that property's name so the
1511
- // caller can unwrap to it, or null when the body is the result itself.
1512
- //
1513
- // Two conditions keep this from firing on a response that IS the entity:
1514
- //
1515
- // 1. EXACTLY ONE property. A body with siblings is a structure in its own
1516
- // right, not a wrapper — `{ok, id}` from a delete, or any paged
1517
- // `{results, next}`, must be handed over whole.
1518
- // 2. The property's SHAPE matches the operation's cardinality. A `list`
1519
- // unwraps only to an array, every other op only to a non-array. So a
1520
- // single-entity op facing `{items: [...]}` is left alone rather than
1521
- // silently yielding a list, and vice versa.
1522
- //
1523
- // A one-field entity whose sole field is itself structured can still be
1524
- // unwrapped wrongly; that is the residual cost of the spec not saying which
1525
- // it means. Naming the wrapper after the entity remains the unambiguous
1526
- // signal, and is still checked first.
1527
1308
  function envelopeProp(resprops, opname) {
1528
1309
  const keys = (0, struct_1.keysof)(resprops);
1529
1310
  if (0 === keys.length) {
1530
1311
  return null;
1531
1312
  }
1532
- // Exactly one STRUCTURED property, with any siblings being scalars.
1533
- //
1534
- // The original rule demanded exactly one property full stop, which missed
1535
- // the single most common envelope shape in the wild:
1536
- //
1537
- // { "success": true, "data": [ ... ] }
1538
- // { "status": "ok", "result": { ... } }
1539
- //
1540
- // A boolean/string status flag beside the payload is metadata, not a
1541
- // sibling of equal standing, so the body is still an envelope. UniVec's
1542
- // /v1/models returns exactly this and every generated SDK — TypeScript, Go,
1543
- // Python alike — returned an empty list against an API plainly serving
1544
- // data.
1545
- //
1546
- // Scalar-only siblings keep the guard meaningful: `{ok, id}` from a delete
1547
- // has no structured member and is still handed over whole, and a body with
1548
- // TWO structured members is a composite we must not guess at.
1549
1313
  const structured = keys.filter((k) => isEntityWrapperProp(resprops[k]));
1550
1314
  if (1 !== structured.length) {
1551
1315
  return null;
@@ -1558,15 +1322,6 @@ function envelopeProp(resprops, opname) {
1558
1322
  }
1559
1323
  return key;
1560
1324
  }
1561
- // An UNTAGGED union: `oneOf`/`anyOf` with two or more real branches and no
1562
- // `discriminator`. Nothing in the schema says which branch a given value is,
1563
- // so no generator can choose a variant, and the field can only be modelled as
1564
- // an open type.
1565
- //
1566
- // Two shapes are deliberately NOT unions to resolve:
1567
- // - a discriminated union — the discriminator names the deciding property;
1568
- // - the nullable idiom `anyOf: [X, {type: 'null'}]`, which is one type that
1569
- // may be absent, not a choice between variants.
1570
1325
  function untaggedUnionBranches(schema) {
1571
1326
  if (null == schema || 'object' !== typeof schema) {
1572
1327
  return 0;
@@ -1581,17 +1336,6 @@ function untaggedUnionBranches(schema) {
1581
1336
  const real = branches.filter((b) => null != b && 'null' !== b.type);
1582
1337
  return real.length < 2 ? 0 : real.length;
1583
1338
  }
1584
- // Deepest/widest untagged union reachable from a field schema, or null when
1585
- // the field is resolvable.
1586
- //
1587
- // The search is RECURSIVE because the union is rarely at the top: in the
1588
- // Typebot Builder spec the `groups` field is an array whose item schema
1589
- // carries 18 untagged unions, the widest 19 branches, 12 levels down. Only a
1590
- // field that bottoms out in such a union has to degrade to an open type, so
1591
- // only a recursive scan can report which fields those are.
1592
- //
1593
- // `seen` guards the self-referential schemas these specs use freely; `depth`
1594
- // is bounded so a pathological document cannot spin.
1595
1339
  function scanUntaggedUnion(schema, depth = 0, seen = new Set()) {
1596
1340
  if (null == schema || 'object' !== typeof schema ||
1597
1341
  seen.has(schema) || depth > MAX_UNION_SCAN_DEPTH) {
@@ -1626,15 +1370,6 @@ function scanUntaggedUnion(schema, depth = 0, seen = new Set()) {
1626
1370
  return 0 === count ? null : { count, branches, depth: at };
1627
1371
  }
1628
1372
  const MAX_UNION_SCAN_DEPTH = 64;
1629
- // Is this schema a collection? null when the schema does not say.
1630
- //
1631
- // `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
1632
- // structured, but a composed schema carries no outer `type` or `items` — so
1633
- // reading those alone silently called it a non-list. A `list` then kept its
1634
- // envelope, and worse, a single-entity op unwrapped to an array-valued
1635
- // property. Composed branches are inspected instead, and unanimity required:
1636
- // a union that is an array in one branch and an object in another does not
1637
- // say what the caller will get, and an envelope is not worth guessing at.
1638
1373
  function propIsList(schema) {
1639
1374
  if (null == schema || 'object' !== typeof schema) {
1640
1375
  return null;
@@ -1657,26 +1392,6 @@ function propIsList(schema) {
1657
1392
  }
1658
1393
  return 'array' === schema.type || null != schema.items;
1659
1394
  }
1660
- // The request BODY a closed schema permits, as a transform mapping.
1661
- //
1662
- // `additionalProperties: false` is the spec saying the server rejects any
1663
- // property it did not declare. When a body says that, sending the caller's
1664
- // whole request payload is wrong: an op's payload also carries its PATH
1665
- // params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
1666
- // request over that one extra key. Restricting the body to the declared
1667
- // properties is then not a heuristic — it is what the spec asked for.
1668
- //
1669
- // Returns null for an open or property-less schema, where `reqdata` (send
1670
- // everything) remains the right default: an open body accepts extras, and
1671
- // with no declared properties there is nothing to restrict to.
1672
- //
1673
- // The KEY is the property's wire name — that is what goes on the wire and
1674
- // what the server matches against. The SOURCE is read by the field's
1675
- // CANONICAL name, because that is the only name the caller ever sees:
1676
- // findFieldDefs runs every property through `canonize(normalizeFieldName())`,
1677
- // so a spec property `UserName` reaches the generated request type as
1678
- // `user_name`. Reading `reqdata.UserName` would find nothing and send
1679
- // undefined.
1680
1395
  function closedBodyTransform(schema) {
1681
1396
  if (null == schema || 'object' !== typeof schema) {
1682
1397
  return null;
@@ -1688,26 +1403,12 @@ function closedBodyTransform(schema) {
1688
1403
  if (0 === names.length) {
1689
1404
  return null;
1690
1405
  }
1691
- // Null prototype: a spec is free to declare a property called `__proto__`,
1692
- // and on an ordinary object that assignment sets the prototype instead of
1693
- // creating an own property — the mapping would vanish, and with it the
1694
- // whole body restriction if it were the only one.
1695
1406
  const out = Object.create(null);
1696
1407
  for (const name of names) {
1697
1408
  out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`';
1698
1409
  }
1699
1410
  return out;
1700
1411
  }
1701
- // The first sentence of `text` (up to a `.`/`!`/`?` followed by whitespace
1702
- // or end), whitespace-collapsed and length-capped with an ellipsis.
1703
- //
1704
- // Shared by the API summary and by a field's `short`, because both answer
1705
- // the same question — give me one line of prose for a place that has room
1706
- // for one line. A field's `short` is interpolated straight into a markdown
1707
- // table cell by every generated Readme, where a raw newline ends the row and
1708
- // takes the rest of the table with it. Specs supply plenty: bullet lists,
1709
- // fenced examples and multi-paragraph notes all appear as a property's
1710
- // `description`, up to 1725 characters of it in the validation corpus.
1711
1412
  function firstSentence(text) {
1712
1413
  const collapsed = text.replace(/\s+/g, ' ').trim();
1713
1414
  const m = collapsed.match(/^(.+?[.!?])(\s|$)/);