@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.
- package/dist/apidef.d.ts +4 -1
- package/dist/apidef.js +12 -34
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/entity.js +0 -33
- package/dist/builder/entity/entity.js.map +1 -1
- package/dist/builder/entity/info.js +0 -4
- package/dist/builder/entity/info.js.map +1 -1
- package/dist/builder/flow.d.ts +4 -1
- package/dist/builder/flow.js +36 -27
- package/dist/builder/flow.js.map +1 -1
- package/dist/guide/graphql01.js +0 -55
- package/dist/guide/graphql01.js.map +1 -1
- package/dist/guide/guide.js +3 -126
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +40 -181
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/parse/graphql.js +0 -25
- package/dist/parse/graphql.js.map +1 -1
- package/dist/parse.js +51 -56
- package/dist/parse.js.map +1 -1
- package/dist/resolved.d.ts +23 -0
- package/dist/resolved.js +92 -0
- package/dist/resolved.js.map +1 -0
- package/dist/resolver.js +0 -2
- package/dist/resolver.js.map +1 -1
- package/dist/transform/args.js +2 -25
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/casecollide.d.ts +3 -0
- package/dist/transform/casecollide.js +67 -0
- package/dist/transform/casecollide.js.map +1 -0
- package/dist/transform/contract.js +43 -38
- package/dist/transform/contract.js.map +1 -1
- package/dist/transform/entity.js +0 -56
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +1 -368
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/flowstep.js +0 -49
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/graphql.js +0 -36
- package/dist/transform/graphql.js.map +1 -1
- package/dist/transform/operation.js +0 -23
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/top.js +1 -128
- package/dist/transform/top.js.map +1 -1
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +17 -316
- package/dist/utility.js.map +1 -1
- package/model/apidef.aon +20 -113
- package/model/guide.aon +1 -32
- package/package.json +5 -5
- package/src/apidef.ts +22 -33
- package/src/builder/entity/entity.ts +0 -33
- package/src/builder/entity/info.ts +0 -4
- package/src/builder/flow.ts +39 -24
- package/src/desc.ts +0 -22
- package/src/guide/graphql01.ts +0 -75
- package/src/guide/guide.ts +5 -127
- package/src/guide/heuristic01.ts +47 -182
- package/src/model.ts +6 -85
- package/src/parse/graphql.ts +0 -25
- package/src/parse.ts +61 -56
- package/src/resolved.ts +136 -0
- package/src/resolver.ts +0 -2
- package/src/transform/args.ts +2 -25
- package/src/transform/casecollide.ts +80 -0
- package/src/transform/contract.ts +44 -34
- package/src/transform/entity.ts +0 -56
- package/src/transform/field.ts +2 -368
- package/src/transform/flowstep.ts +0 -49
- package/src/transform/graphql.ts +0 -36
- package/src/transform/operation.ts +0 -23
- package/src/transform/top.ts +1 -131
- package/src/transform.ts +0 -1
- package/src/types.ts +4 -13
- package/src/utility.ts +18 -314
package/src/utility.ts
CHANGED
|
@@ -110,35 +110,6 @@ function formatJsonSrc(jsonsrc: string) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
// Common irregular plurals, in the form plural → singular. Used at the
|
|
114
|
-
// head of depluralize() to short-circuit cases the suffix rules below
|
|
115
|
-
// would otherwise mishandle.
|
|
116
|
-
//
|
|
117
|
-
// Three over-strip classes are worked around here because the surface
|
|
118
|
-
// form gives no clean discriminator:
|
|
119
|
-
//
|
|
120
|
-
// * `-Vse+s` plurals (houses, phases, noses, …) would hit the
|
|
121
|
-
// generic `-ses → ∅` rule and become hous/phas/nos. Every such
|
|
122
|
-
// -se+s plural needs an explicit entry.
|
|
123
|
-
//
|
|
124
|
-
// * `-che+s` plurals (caches, niches, headaches, …) would hit the
|
|
125
|
-
// generic `-ches → ∅` rule and become cach/nich/headach. Same
|
|
126
|
-
// pattern: no letter-doubling tell exists (cache vs church both
|
|
127
|
-
// have a single 'ch'), so each -che singular is enumerated.
|
|
128
|
-
//
|
|
129
|
-
// * `-oe+s` plurals (shoes, canoes, oboes) would hit the generic
|
|
130
|
-
// `-oes → -o` rule (for potatoes/heroes) and become sho/cano/obo.
|
|
131
|
-
// Only collision-safe entries are listed: a key must not also be a
|
|
132
|
-
// suffix of a real `-o`+es plural (e.g. `toes` is excluded because
|
|
133
|
-
// it would turn tomatoes → tomatoe).
|
|
134
|
-
//
|
|
135
|
-
// Keys are lowercase; depluralize() does a case-insensitive lookup
|
|
136
|
-
// and reapplies the caller's casing on the way out.
|
|
137
|
-
//
|
|
138
|
-
// Null-prototype: these tables are indexed by spec-derived names, so a
|
|
139
|
-
// schema or path segment called `constructor` / `__proto__` / `toString`
|
|
140
|
-
// would otherwise resolve to the inherited Object member and be returned
|
|
141
|
-
// as a "match" — crashing matchCase() on a function. See NULL_PROTO_NOTE.
|
|
142
113
|
const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
|
|
143
114
|
'analytics': 'analytics',
|
|
144
115
|
'analyses': 'analysis',
|
|
@@ -205,16 +176,6 @@ const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
|
|
|
205
176
|
'yes': 'yes',
|
|
206
177
|
})
|
|
207
178
|
|
|
208
|
-
// NULL_PROTO_NOTE: every plain-object lookup table in this module whose keys
|
|
209
|
-
// come from an API spec is built with a null prototype
|
|
210
|
-
// (`Object.assign(Object.create(null), {...})`). Without it, `TABLE[name]`
|
|
211
|
-
// inherits from Object.prototype, so `TABLE['constructor']` yields the Object
|
|
212
|
-
// constructor (truthy, a function) and `TABLE['__proto__']` yields
|
|
213
|
-
// Object.prototype (truthy, an object). Both then flow into code expecting a
|
|
214
|
-
// string. A spec with `components.schemas.Constructor` is enough to reach
|
|
215
|
-
// this: canonize -> depluralize -> matchCase -> `.toLowerCase is not a
|
|
216
|
-
// function`, failing the whole build at the guide stage. Keep new tables
|
|
217
|
-
// null-prototype, or use a Map (CANONIZE_CACHE already does).
|
|
218
179
|
|
|
219
180
|
// Sorted longest-first so the most specific IRREGULARS suffix wins.
|
|
220
181
|
// Without this, 'women' would be shadowed by 'men' (3 < 5) under
|
|
@@ -223,11 +184,6 @@ const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
|
|
|
223
184
|
const IRREGULAR_KEYS = Object.keys(IRREGULARS).sort((a, b) => b.length - a.length)
|
|
224
185
|
|
|
225
186
|
|
|
226
|
-
// Reapply the case pattern of `source` to `target`. Used so the
|
|
227
|
-
// case-insensitive lookups in depluralize() preserve the caller's
|
|
228
|
-
// casing on the way out (HOUSES → HOUSE, Houses → House, houses →
|
|
229
|
-
// house). Falls through to `target` unchanged for mixed-case sources
|
|
230
|
-
// that don't fit one of the three canonical patterns.
|
|
231
187
|
function matchCase(source: string, target: string): string {
|
|
232
188
|
if (source === source.toLowerCase()) return target.toLowerCase()
|
|
233
189
|
if (source === source.toUpperCase()) return target.toUpperCase()
|
|
@@ -238,20 +194,6 @@ function matchCase(source: string, target: string): string {
|
|
|
238
194
|
}
|
|
239
195
|
|
|
240
196
|
|
|
241
|
-
// Per-model plural overrides, populated from the model's
|
|
242
|
-
// `main.custom.plurals` section at apidef pipeline entry and cleared
|
|
243
|
-
// between runs. Checked by depluralize() before the built-in
|
|
244
|
-
// IRREGULARS table and rule chain — so a model can override any
|
|
245
|
-
// default depluralization, including correct-by-default cases, when
|
|
246
|
-
// its domain demands a different singular (e.g. fitness API with
|
|
247
|
-
// {axes: axe}, photography app with {lenses: lens}).
|
|
248
|
-
//
|
|
249
|
-
// Module-level rather than a parameter so the many existing
|
|
250
|
-
// depluralize/canonize call sites across transforms and guide
|
|
251
|
-
// inherit the override without signature churn. apidef is
|
|
252
|
-
// single-model-per-process; if that ever changes, switch this to a
|
|
253
|
-
// per-context map.
|
|
254
|
-
// Null-prototype: see NULL_PROTO_NOTE above.
|
|
255
197
|
let CUSTOM_PLURALS: Record<string, string> = Object.create(null)
|
|
256
198
|
let CUSTOM_PLURAL_KEYS: string[] = []
|
|
257
199
|
|
|
@@ -323,7 +265,6 @@ function depluralize(word: string): string {
|
|
|
323
265
|
// rules add a letter, so they need to match the case of the dropped
|
|
324
266
|
// suffix; all other rules just slice and inherit the caller's case.
|
|
325
267
|
|
|
326
|
-
// -ies -> -y (cities -> city), but only if result is > 2 chars
|
|
327
268
|
if (lower.endsWith('ies') && word.length > 3) {
|
|
328
269
|
const dropped = word.slice(-3)
|
|
329
270
|
const y = dropped === dropped.toUpperCase() ? 'Y' : 'y'
|
|
@@ -356,12 +297,6 @@ function depluralize(word: string): string {
|
|
|
356
297
|
return word.slice(0, -1)
|
|
357
298
|
}
|
|
358
299
|
|
|
359
|
-
// -zes plurals come from -ze singulars (prize, size, freeze, maze,
|
|
360
|
-
// breeze, …) far more often than from a bare -z taking -es. The only
|
|
361
|
-
// -zes plurals that strip the full -es have a doubled-z stem
|
|
362
|
-
// (buzz/buzzes, fez/fezzes). Discriminate on -zzes so prizes → prize
|
|
363
|
-
// instead of priz. Mirrors the -ses/-Vse+s problem the IRREGULARS
|
|
364
|
-
// table works around for the -se case.
|
|
365
300
|
if (lower.endsWith('zzes')) {
|
|
366
301
|
return word.slice(0, -2)
|
|
367
302
|
}
|
|
@@ -375,7 +310,6 @@ function depluralize(word: string): string {
|
|
|
375
310
|
return word.slice(0, -2)
|
|
376
311
|
}
|
|
377
312
|
|
|
378
|
-
// -s -> remove -s (cats -> cat), but only if result is > 2 chars
|
|
379
313
|
if (lower.endsWith('s') &&
|
|
380
314
|
!lower.endsWith('ss') &&
|
|
381
315
|
!lower.endsWith('us') &&
|
|
@@ -391,8 +325,11 @@ function depluralize(word: string): string {
|
|
|
391
325
|
|
|
392
326
|
function find(obj: any, qkey: string): any[] {
|
|
393
327
|
const vals: any[] = []
|
|
328
|
+
const seen = new WeakSet<object>()
|
|
394
329
|
const collect = (o: any): void => {
|
|
395
330
|
if (!o || 'object' !== typeof o) return
|
|
331
|
+
if (seen.has(o)) return
|
|
332
|
+
seen.add(o)
|
|
396
333
|
if (Array.isArray(o)) {
|
|
397
334
|
for (let i = 0; i < o.length; i++) collect(o[i])
|
|
398
335
|
} else {
|
|
@@ -486,7 +423,6 @@ function $APPEND(inj: Injection, val: any, ref: any, store: any) {
|
|
|
486
423
|
const vstore = { ...store }
|
|
487
424
|
vstore.$TOP = { [key]: dval }
|
|
488
425
|
|
|
489
|
-
// const ptval = transform({ [key]: dval }, { [key]: xform }, {
|
|
490
426
|
const ptval = inject({ [key]: xform }, vstore, {
|
|
491
427
|
meta: { ...inj.meta },
|
|
492
428
|
errs: inj.errs,
|
|
@@ -542,7 +478,6 @@ function $SELECT(inj: Injection, _val: any, _ref: any, store: any) {
|
|
|
542
478
|
}
|
|
543
479
|
}
|
|
544
480
|
|
|
545
|
-
// TODO: select should be safe for scalars
|
|
546
481
|
const children = select(dparents, selector)
|
|
547
482
|
|
|
548
483
|
if (0 < children.length) {
|
|
@@ -554,8 +489,6 @@ function $SELECT(inj: Injection, _val: any, _ref: any, store: any) {
|
|
|
554
489
|
meta: merge([
|
|
555
490
|
inj.meta,
|
|
556
491
|
|
|
557
|
-
// TODO: need this hack as struct does not provide a way to get grandparent keys
|
|
558
|
-
// also, these capture actions are not preserving the path!
|
|
559
492
|
{ select: { key: { [slice(inj.path, 1, -1).join('+')]: child.$KEY } } }
|
|
560
493
|
]),
|
|
561
494
|
errs: inj.errs,
|
|
@@ -574,8 +507,6 @@ function $RECASE(inj: Injection, val: any, ref: any, store: any) {
|
|
|
574
507
|
) {
|
|
575
508
|
const dval = inj.parent[inj.key]
|
|
576
509
|
|
|
577
|
-
// TODO: handle paths more generally! use inj.prior?
|
|
578
|
-
// TODO: mkae this into a utility method on inj?
|
|
579
510
|
const dkey = inj.prior.key
|
|
580
511
|
const gkey = inj.prior.prior.key
|
|
581
512
|
|
|
@@ -603,13 +534,6 @@ function $RECASE(inj: Injection, val: any, ref: any, store: any) {
|
|
|
603
534
|
|
|
604
535
|
type PathMatch = (string[] & { index: number, expr: string, path: string })
|
|
605
536
|
|
|
606
|
-
// A special-purpose regex-style matcher for url paths.
|
|
607
|
-
// t - text part
|
|
608
|
-
// p - param part
|
|
609
|
-
// / - part separator
|
|
610
|
-
// / at start - must match from start
|
|
611
|
-
// / at end - must match to end
|
|
612
|
-
// See utility.test.ts for examples
|
|
613
537
|
function pathMatch(path: string | string[], expr: string):
|
|
614
538
|
null | PathMatch {
|
|
615
539
|
|
|
@@ -744,9 +668,6 @@ function formatJSONIC(
|
|
|
744
668
|
'`' + JSON.stringify(v)
|
|
745
669
|
.substring(1)
|
|
746
670
|
.replace(/\\n/g, '\n')
|
|
747
|
-
// Inside a JSONIC backtick literal a double quote is a literal
|
|
748
|
-
// character, so unescape JSON's \" back to " (was previously
|
|
749
|
-
// replaced with ':' which silently corrupted quoted text).
|
|
750
671
|
.replace(/\\"/g, '"')
|
|
751
672
|
.replace(/`/g, '\\`')
|
|
752
673
|
.replace(/"$/, '`'))
|
|
@@ -783,11 +704,6 @@ function renderJSONIC(
|
|
|
783
704
|
c: (color: any, text: string) => string,
|
|
784
705
|
renderPrimitive: (v: any) => string,
|
|
785
706
|
renderComment: (c: any) => string | null,
|
|
786
|
-
// True once the value has already been through decircular. `seen` never
|
|
787
|
-
// forgets a node, so a merely REPEATED reference (a shared node in a DAG,
|
|
788
|
-
// not a cycle) also lands in the fallback below. If decircular leaves such
|
|
789
|
-
// a repeat in place, retrying forever overflows the stack — which is what
|
|
790
|
-
// formatting a deep validation error used to do. Retry at most once.
|
|
791
707
|
decircularized: boolean,
|
|
792
708
|
): string {
|
|
793
709
|
|
|
@@ -954,16 +870,6 @@ function renderJSONIC(
|
|
|
954
870
|
}
|
|
955
871
|
|
|
956
872
|
|
|
957
|
-
// Canonical type-sentinel vocabulary. VALID_CANON maps an OpenAPI type NAME
|
|
958
|
-
// to its `$SENTINEL` form; CANON_ONE is the union sentinel produced by
|
|
959
|
-
// `validator` for a multi-type (`['`$ONE`', [member, ...]]`). Both are part
|
|
960
|
-
// of the public API so downstream consumers (e.g. @voxgig/sdkgen's
|
|
961
|
-
// sentinel -> language-type table) can verify they cover the full set
|
|
962
|
-
// instead of hand-syncing against this file.
|
|
963
|
-
//
|
|
964
|
-
// Null-prototype (see NULL_PROTO_NOTE): `type` values come from the spec, so
|
|
965
|
-
// a schema declaring `type: constructor` would otherwise return the Object
|
|
966
|
-
// constructor here rather than falling through to the 'Any' default.
|
|
967
873
|
const VALID_CANON: Record<string, string> = Object.assign(Object.create(null), {
|
|
968
874
|
'string': '`$STRING`',
|
|
969
875
|
'number': '`$NUMBER`',
|
|
@@ -1001,62 +907,26 @@ function transliterate(s: string): string {
|
|
|
1001
907
|
|
|
1002
908
|
const CANONIZE_CACHE = new Map<string, string>()
|
|
1003
909
|
|
|
910
|
+
// A trailing plural after an acronym is part of the word, and snakify would
|
|
911
|
+
// split it letter by letter. See docs/design/derived-names.md
|
|
912
|
+
const ACRONYM_PLURAL_RE = /([A-Z]{2,})s(?![a-zA-Z])/g
|
|
913
|
+
|
|
1004
914
|
function canonize(s: string) {
|
|
1005
915
|
if (null == s || '' === s) return ''
|
|
1006
916
|
const cached = CANONIZE_CACHE.get(s)
|
|
1007
917
|
if (undefined !== cached) return cached
|
|
1008
|
-
const
|
|
918
|
+
const deacronymed = transliterate(s)
|
|
919
|
+
.replace(FILE_EXT_RE, '')
|
|
920
|
+
.replace(ACRONYM_PLURAL_RE, (_m, run) => run[0] + run.slice(1).toLowerCase() + 's')
|
|
921
|
+
const out = depluralize(snakify(deacronymed))
|
|
1009
922
|
.replace(/[^a-zA-Z_0-9]/g, '')
|
|
1010
923
|
CANONIZE_CACHE.set(s, out)
|
|
1011
924
|
return out
|
|
1012
925
|
}
|
|
1013
926
|
|
|
1014
927
|
|
|
1015
|
-
// Canonicalise a FIELD name — which is a WIRE identifier, not a type name.
|
|
1016
|
-
//
|
|
1017
|
-
// `canonize` is right for entity/type names: it snakifies and depluralizes so
|
|
1018
|
-
// `Users` and `user-items` converge on `user` / `user_item`. Applied to a
|
|
1019
|
-
// field it is actively WRONG, because the name has to match the JSON the
|
|
1020
|
-
// server actually sends:
|
|
1021
|
-
//
|
|
1022
|
-
// modelType -> canonize -> model_type (server sends modelType)
|
|
1023
|
-
// items -> canonize -> item (server sends items)
|
|
1024
|
-
//
|
|
1025
|
-
// Nothing maps back: the model's `alias.field` map is emitted empty and no
|
|
1026
|
-
// generator consumes it, so the wire name is simply lost. Across the fleet's
|
|
1027
|
-
// specs that renamed 23% of all fields (146 repos) and depluralized another
|
|
1028
|
-
// 13% — every one of those SDKs reading a key the server never sends.
|
|
1029
|
-
//
|
|
1030
|
-
// So: keep the transliteration and identifier sanitisation that stop a name
|
|
1031
|
-
// being unusable in a target language, and drop the snakify/depluralize that
|
|
1032
|
-
// change what the name MEANS. Case and plurality are preserved verbatim.
|
|
1033
928
|
const CANONIZE_FIELD_CACHE = new Map<string, string>()
|
|
1034
929
|
|
|
1035
|
-
// Re-split a boundary-less entity name using the component name's boundaries.
|
|
1036
|
-
//
|
|
1037
|
-
// ONLY when the name has no boundaries of its own. A name that already
|
|
1038
|
-
// contains `_` was split by something that knew where the words were, and is
|
|
1039
|
-
// left exactly as it is.
|
|
1040
|
-
//
|
|
1041
|
-
// ONLY on an EXACT concatenation match, which is what stops this inventing
|
|
1042
|
-
// anything. The component's segments are accumulated left to right and the
|
|
1043
|
-
// result is used only if it equals the path-derived name with its separators
|
|
1044
|
-
// removed:
|
|
1045
|
-
//
|
|
1046
|
-
// payeeverification + payee_verification_result
|
|
1047
|
-
// payee -> "payee" no
|
|
1048
|
-
// payee_verification -> "payeeverification" YES -> payee_verification
|
|
1049
|
-
//
|
|
1050
|
-
// virtual + payment_account
|
|
1051
|
-
// payment -> "payment" no
|
|
1052
|
-
// payment_account -> "paymentaccount" no -> unchanged
|
|
1053
|
-
//
|
|
1054
|
-
// So a component that has nothing to do with the path token cannot rename it,
|
|
1055
|
-
// and a component that merely EXTENDS the token (`payment_account_entry` for
|
|
1056
|
-
// `payment`) cannot either -- only the prefix that reconstructs the token
|
|
1057
|
-
// exactly is taken. A different entity name is never reachable from here; the
|
|
1058
|
-
// same letters in the same order, with separators restored, is the whole of
|
|
1059
|
-
// what this can produce.
|
|
1060
930
|
function resplitFromCmp(entname: string, cmp: string, why: string[]): string {
|
|
1061
931
|
if (null == entname || '' === entname || entname.includes('_')) {
|
|
1062
932
|
return entname
|
|
@@ -1097,12 +967,6 @@ function canonizeField(s: string) {
|
|
|
1097
967
|
}
|
|
1098
968
|
|
|
1099
969
|
|
|
1100
|
-
// Namespace-qualified schema names (ASP.NET / Java style:
|
|
1101
|
-
// "NoFrixion.MoneyMoov.Models.PaymentRequests.MerchantPayment",
|
|
1102
|
-
// "com.example.api.Payment") describe the type by their LAST dotted
|
|
1103
|
-
// segment; the namespace prefix is packaging noise. Reduce to the last
|
|
1104
|
-
// meaningful segment — skipping version-ish ("v2", "10") or too-short
|
|
1105
|
-
// tails — so entity names derive from the type, not the namespace.
|
|
1106
970
|
function stripSchemaNamespace(name: string): string {
|
|
1107
971
|
if (null == name || !name.includes('.')) return name
|
|
1108
972
|
const segs = name.split('.')
|
|
@@ -1128,24 +992,6 @@ function canonizeCmpName(orig: string): string {
|
|
|
1128
992
|
const FIRST_LETTER_RE = /[a-zA-Z]/
|
|
1129
993
|
|
|
1130
994
|
|
|
1131
|
-
// No target language permits an identifier that starts with a digit, so a
|
|
1132
|
-
// name derived from one — a `3dsSession` schema, a `/2fa` path segment, a
|
|
1133
|
-
// `_3DSecure` GraphQL type — is prefixed with an `n`.
|
|
1134
|
-
//
|
|
1135
|
-
// The prefix takes the case of the name it guards: lower for `3ds_session`,
|
|
1136
|
-
// upper for `3DSecure`. That keeps the result inside whatever casing
|
|
1137
|
-
// convention the caller was already working in, so a later PascalCase or
|
|
1138
|
-
// camelCase conversion has nothing to undo. A name with no letter in it at
|
|
1139
|
-
// all (`404`) takes the lower-case prefix.
|
|
1140
|
-
//
|
|
1141
|
-
// This is the ONE place the rule lives. Entity names reach it through
|
|
1142
|
-
// `ensureMinEntityName` and the GraphQL guide's `entityName`; project slugs
|
|
1143
|
-
// through `sanitizeSlug`.
|
|
1144
|
-
//
|
|
1145
|
-
// FIELD names deliberately do NOT come here. A field name is a WIRE
|
|
1146
|
-
// identifier and renaming it makes the SDK read a key the server never sends
|
|
1147
|
-
// — the mistake `canonizeField` exists to document. Targets escape those at
|
|
1148
|
-
// the point of emission instead.
|
|
1149
995
|
function prefixLeadingDigit(s: string): string {
|
|
1150
996
|
if (null == s || '' === s) return s
|
|
1151
997
|
const first = s.charCodeAt(0)
|
|
@@ -1260,13 +1106,6 @@ function ensureMinEntityName(
|
|
|
1260
1106
|
}
|
|
1261
1107
|
|
|
1262
1108
|
if (padded !== name && null != existing[padded]) {
|
|
1263
|
-
// The name was modified (truncated/sanitized) and collides with an
|
|
1264
|
-
// existing entity. Only a collision between DIFFERENT origins needs a
|
|
1265
|
-
// numeric suffix — the same original name re-encountered (e.g. the same
|
|
1266
|
-
// long schema referenced by several methods on one path) must reuse the
|
|
1267
|
-
// existing entity so its ops merge instead of minting phantom
|
|
1268
|
-
// "<entity>2/3/4" entities. Entities record their pre-truncation name
|
|
1269
|
-
// as `longname`; entries without one keep the old always-suffix rule.
|
|
1270
1109
|
if (existing[padded].longname === name) {
|
|
1271
1110
|
return padded
|
|
1272
1111
|
}
|
|
@@ -1287,24 +1126,8 @@ function ensureMinEntityName(
|
|
|
1287
1126
|
// Unconditional suffixes: framework noise, always stripped.
|
|
1288
1127
|
const CMP_SUFFIXES = ['_rest_controller', '_controller', '_response', '_request']
|
|
1289
1128
|
|
|
1290
|
-
// Guarded suffixes: pagination wrappers ('_page_response', '_page') and
|
|
1291
|
-
// op-reply wrappers ('_create_response', '_update_response') fold wrapper
|
|
1292
|
-
// schemas (BeneficiaryPageResponse, MerchantTokenPage,
|
|
1293
|
-
// BeneficiariesCreateResponse, ...) into their base entity — but ONLY when
|
|
1294
|
-
// the remainder is itself a known component schema (the wrapper
|
|
1295
|
-
// convention). Without that guard a real noun gets mangled: an API whose
|
|
1296
|
-
// resource IS a page (LandingPage entity at /landing-pages) must keep
|
|
1297
|
-
// 'landing_page', not become 'landing'. Order matters: longer first, since
|
|
1298
|
-
// '_page_response'/'_create_response' also end with '_response'. Bare
|
|
1299
|
-
// '_create'/'_update' are never stripped: too likely part of a real noun.
|
|
1300
1129
|
const CMP_GUARDED_SUFFIXES = ['_create_response', '_update_response', '_page_response', '_page']
|
|
1301
1130
|
|
|
1302
|
-
// Op-reply wrappers of the form <namespace><Verb>Result (incident.io:
|
|
1303
|
-
// AlertsResolveResultV2, SeveritiesListResultV1). Same idea as
|
|
1304
|
-
// '_create_response' above, but the verb varies and sits before the wrapper
|
|
1305
|
-
// word rather than being part of a fixed pair, so the shape is matched rather
|
|
1306
|
-
// than enumerated. Guarded exactly like the others: nothing is stripped
|
|
1307
|
-
// unless the remainder is a real schema.
|
|
1308
1131
|
const CMP_RESULT_VERBS = [
|
|
1309
1132
|
'list', 'create', 'show', 'update', 'delete', 'remove',
|
|
1310
1133
|
'get', 'edit', 'resolve', 'rotate', 'search',
|
|
@@ -1322,26 +1145,6 @@ function guideActive(node: any): boolean {
|
|
|
1322
1145
|
}
|
|
1323
1146
|
|
|
1324
1147
|
|
|
1325
|
-
// An API's ACCESS-TOKEN EXCHANGE is not a resource, and must not become an
|
|
1326
|
-
// entity. The shape apidef looks for is the one every such endpoint has:
|
|
1327
|
-
//
|
|
1328
|
-
// 1. The spec as a whole is SECURED (a top-level `security` requirement).
|
|
1329
|
-
// Without that, a per-operation `security: []` clears nothing and
|
|
1330
|
-
// carries no signal at all.
|
|
1331
|
-
// 2. The operation clears that requirement with its own `security: []` —
|
|
1332
|
-
// it is the one call a client can make before it holds a credential,
|
|
1333
|
-
// because it is what issues them.
|
|
1334
|
-
// 3. It is a POST. A credential exchange writes; a GET that happens to
|
|
1335
|
-
// return a field called `token` is far likelier to be a resource.
|
|
1336
|
-
// 4. Its success response carries a TOKEN-shaped field.
|
|
1337
|
-
//
|
|
1338
|
-
// All four together, or it is a resource. There is deliberately no vendor
|
|
1339
|
-
// extension and no overlay to say otherwise (ADR-002): a spec apidef does
|
|
1340
|
-
// not control cannot be annotated anyway, and a heuristic that can be
|
|
1341
|
-
// corrected in guide.aon needs no second correction surface.
|
|
1342
|
-
//
|
|
1343
|
-
// Returns the field names the exchange uses, which is what sdkgen's
|
|
1344
|
-
// `secrets` feature needs to drive it, or null when this is a resource.
|
|
1345
1148
|
const AUTH_TOKEN_FIELDS = [
|
|
1346
1149
|
'access_token', 'accessToken', 'access-token',
|
|
1347
1150
|
'id_token', 'idToken',
|
|
@@ -1367,8 +1170,6 @@ function authExchangeOp(
|
|
|
1367
1170
|
return null
|
|
1368
1171
|
}
|
|
1369
1172
|
|
|
1370
|
-
// An empty ARRAY, specifically. `security` absent means "inherit the
|
|
1371
|
-
// global requirement"; `security: []` means "no credential needed here".
|
|
1372
1173
|
if (!Array.isArray(op?.security) || 0 !== op.security.length) {
|
|
1373
1174
|
return null
|
|
1374
1175
|
}
|
|
@@ -1426,9 +1227,6 @@ function schemaProps(schema: any): string[] {
|
|
|
1426
1227
|
}
|
|
1427
1228
|
|
|
1428
1229
|
|
|
1429
|
-
// First name in `names` that the schema declares, compared case-insensitively
|
|
1430
|
-
// so `Access_Token` matches `access_token`. Ordered by the CANDIDATE list, not
|
|
1431
|
-
// by declaration order, so `access_token` wins over a sibling `token`.
|
|
1432
1230
|
function firstFieldMatch(props: string[], names: string[]): string | null {
|
|
1433
1231
|
const lower = new Map<string, string>()
|
|
1434
1232
|
for (const p of props) {
|
|
@@ -1556,6 +1354,12 @@ function warnOnError(where: string, warn: Warner, fn: Function, result?: any) {
|
|
|
1556
1354
|
|
|
1557
1355
|
|
|
1558
1356
|
|
|
1357
|
+
function debugpathOn(): boolean {
|
|
1358
|
+
const apipath = process.env.APIDEF_DEBUG_PATH
|
|
1359
|
+
return null != apipath && '' !== apipath
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
|
|
1559
1363
|
function debugpath(pathStr: string, methodName: string | null | undefined, ...args: any[]): void {
|
|
1560
1364
|
const apipath = process.env.APIDEF_DEBUG_PATH
|
|
1561
1365
|
|
|
@@ -1567,7 +1371,6 @@ function debugpath(pathStr: string, methodName: string | null | undefined, ...ar
|
|
|
1567
1371
|
// Check if path matches
|
|
1568
1372
|
if (pathStr !== targetPath) return
|
|
1569
1373
|
|
|
1570
|
-
// If a method is specified in apipath and we have a method name, check if it matches
|
|
1571
1374
|
if (targetMethod && methodName) {
|
|
1572
1375
|
if (methodName.toLowerCase() !== targetMethod.toLowerCase()) return
|
|
1573
1376
|
}
|
|
@@ -1608,7 +1411,6 @@ function findPathsWithPrefix(
|
|
|
1608
1411
|
}
|
|
1609
1412
|
|
|
1610
1413
|
|
|
1611
|
-
// TODO: move to jostraca?
|
|
1612
1414
|
function allcapify(s?: string) {
|
|
1613
1415
|
return 'string' === typeof s ? snakify(s).toUpperCase() : ''
|
|
1614
1416
|
}
|
|
@@ -1764,48 +1566,12 @@ function isEntityWrapperProp(propSchema: any): boolean {
|
|
|
1764
1566
|
}
|
|
1765
1567
|
|
|
1766
1568
|
|
|
1767
|
-
// A response body that is nothing but a single wrapper property is an
|
|
1768
|
-
// ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
|
|
1769
|
-
// `{items: [...]}`, `{results: [...]}`. Return that property's name so the
|
|
1770
|
-
// caller can unwrap to it, or null when the body is the result itself.
|
|
1771
|
-
//
|
|
1772
|
-
// Two conditions keep this from firing on a response that IS the entity:
|
|
1773
|
-
//
|
|
1774
|
-
// 1. EXACTLY ONE property. A body with siblings is a structure in its own
|
|
1775
|
-
// right, not a wrapper — `{ok, id}` from a delete, or any paged
|
|
1776
|
-
// `{results, next}`, must be handed over whole.
|
|
1777
|
-
// 2. The property's SHAPE matches the operation's cardinality. A `list`
|
|
1778
|
-
// unwraps only to an array, every other op only to a non-array. So a
|
|
1779
|
-
// single-entity op facing `{items: [...]}` is left alone rather than
|
|
1780
|
-
// silently yielding a list, and vice versa.
|
|
1781
|
-
//
|
|
1782
|
-
// A one-field entity whose sole field is itself structured can still be
|
|
1783
|
-
// unwrapped wrongly; that is the residual cost of the spec not saying which
|
|
1784
|
-
// it means. Naming the wrapper after the entity remains the unambiguous
|
|
1785
|
-
// signal, and is still checked first.
|
|
1786
1569
|
function envelopeProp(resprops: any, opname: string): string | null {
|
|
1787
1570
|
const keys = keysof(resprops)
|
|
1788
1571
|
if (0 === keys.length) {
|
|
1789
1572
|
return null
|
|
1790
1573
|
}
|
|
1791
1574
|
|
|
1792
|
-
// Exactly one STRUCTURED property, with any siblings being scalars.
|
|
1793
|
-
//
|
|
1794
|
-
// The original rule demanded exactly one property full stop, which missed
|
|
1795
|
-
// the single most common envelope shape in the wild:
|
|
1796
|
-
//
|
|
1797
|
-
// { "success": true, "data": [ ... ] }
|
|
1798
|
-
// { "status": "ok", "result": { ... } }
|
|
1799
|
-
//
|
|
1800
|
-
// A boolean/string status flag beside the payload is metadata, not a
|
|
1801
|
-
// sibling of equal standing, so the body is still an envelope. UniVec's
|
|
1802
|
-
// /v1/models returns exactly this and every generated SDK — TypeScript, Go,
|
|
1803
|
-
// Python alike — returned an empty list against an API plainly serving
|
|
1804
|
-
// data.
|
|
1805
|
-
//
|
|
1806
|
-
// Scalar-only siblings keep the guard meaningful: `{ok, id}` from a delete
|
|
1807
|
-
// has no structured member and is still handed over whole, and a body with
|
|
1808
|
-
// TWO structured members is a composite we must not guess at.
|
|
1809
1575
|
const structured = keys.filter((k: string) => isEntityWrapperProp(resprops[k]))
|
|
1810
1576
|
if (1 !== structured.length) {
|
|
1811
1577
|
return null
|
|
@@ -1823,15 +1589,6 @@ function envelopeProp(resprops: any, opname: string): string | null {
|
|
|
1823
1589
|
}
|
|
1824
1590
|
|
|
1825
1591
|
|
|
1826
|
-
// An UNTAGGED union: `oneOf`/`anyOf` with two or more real branches and no
|
|
1827
|
-
// `discriminator`. Nothing in the schema says which branch a given value is,
|
|
1828
|
-
// so no generator can choose a variant, and the field can only be modelled as
|
|
1829
|
-
// an open type.
|
|
1830
|
-
//
|
|
1831
|
-
// Two shapes are deliberately NOT unions to resolve:
|
|
1832
|
-
// - a discriminated union — the discriminator names the deciding property;
|
|
1833
|
-
// - the nullable idiom `anyOf: [X, {type: 'null'}]`, which is one type that
|
|
1834
|
-
// may be absent, not a choice between variants.
|
|
1835
1592
|
function untaggedUnionBranches(schema: any): number {
|
|
1836
1593
|
if (null == schema || 'object' !== typeof schema) {
|
|
1837
1594
|
return 0
|
|
@@ -1848,17 +1605,6 @@ function untaggedUnionBranches(schema: any): number {
|
|
|
1848
1605
|
}
|
|
1849
1606
|
|
|
1850
1607
|
|
|
1851
|
-
// Deepest/widest untagged union reachable from a field schema, or null when
|
|
1852
|
-
// the field is resolvable.
|
|
1853
|
-
//
|
|
1854
|
-
// The search is RECURSIVE because the union is rarely at the top: in the
|
|
1855
|
-
// Typebot Builder spec the `groups` field is an array whose item schema
|
|
1856
|
-
// carries 18 untagged unions, the widest 19 branches, 12 levels down. Only a
|
|
1857
|
-
// field that bottoms out in such a union has to degrade to an open type, so
|
|
1858
|
-
// only a recursive scan can report which fields those are.
|
|
1859
|
-
//
|
|
1860
|
-
// `seen` guards the self-referential schemas these specs use freely; `depth`
|
|
1861
|
-
// is bounded so a pathological document cannot spin.
|
|
1862
1608
|
function scanUntaggedUnion(
|
|
1863
1609
|
schema: any,
|
|
1864
1610
|
depth: number = 0,
|
|
@@ -1904,15 +1650,6 @@ function scanUntaggedUnion(
|
|
|
1904
1650
|
const MAX_UNION_SCAN_DEPTH = 64
|
|
1905
1651
|
|
|
1906
1652
|
|
|
1907
|
-
// Is this schema a collection? null when the schema does not say.
|
|
1908
|
-
//
|
|
1909
|
-
// `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
|
|
1910
|
-
// structured, but a composed schema carries no outer `type` or `items` — so
|
|
1911
|
-
// reading those alone silently called it a non-list. A `list` then kept its
|
|
1912
|
-
// envelope, and worse, a single-entity op unwrapped to an array-valued
|
|
1913
|
-
// property. Composed branches are inspected instead, and unanimity required:
|
|
1914
|
-
// a union that is an array in one branch and an object in another does not
|
|
1915
|
-
// say what the caller will get, and an envelope is not worth guessing at.
|
|
1916
1653
|
function propIsList(schema: any): boolean | null {
|
|
1917
1654
|
if (null == schema || 'object' !== typeof schema) {
|
|
1918
1655
|
return null
|
|
@@ -1939,26 +1676,6 @@ function propIsList(schema: any): boolean | null {
|
|
|
1939
1676
|
}
|
|
1940
1677
|
|
|
1941
1678
|
|
|
1942
|
-
// The request BODY a closed schema permits, as a transform mapping.
|
|
1943
|
-
//
|
|
1944
|
-
// `additionalProperties: false` is the spec saying the server rejects any
|
|
1945
|
-
// property it did not declare. When a body says that, sending the caller's
|
|
1946
|
-
// whole request payload is wrong: an op's payload also carries its PATH
|
|
1947
|
-
// params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
|
|
1948
|
-
// request over that one extra key. Restricting the body to the declared
|
|
1949
|
-
// properties is then not a heuristic — it is what the spec asked for.
|
|
1950
|
-
//
|
|
1951
|
-
// Returns null for an open or property-less schema, where `reqdata` (send
|
|
1952
|
-
// everything) remains the right default: an open body accepts extras, and
|
|
1953
|
-
// with no declared properties there is nothing to restrict to.
|
|
1954
|
-
//
|
|
1955
|
-
// The KEY is the property's wire name — that is what goes on the wire and
|
|
1956
|
-
// what the server matches against. The SOURCE is read by the field's
|
|
1957
|
-
// CANONICAL name, because that is the only name the caller ever sees:
|
|
1958
|
-
// findFieldDefs runs every property through `canonize(normalizeFieldName())`,
|
|
1959
|
-
// so a spec property `UserName` reaches the generated request type as
|
|
1960
|
-
// `user_name`. Reading `reqdata.UserName` would find nothing and send
|
|
1961
|
-
// undefined.
|
|
1962
1679
|
function closedBodyTransform(schema: any): Record<string, string> | null {
|
|
1963
1680
|
if (null == schema || 'object' !== typeof schema) {
|
|
1964
1681
|
return null
|
|
@@ -1972,10 +1689,6 @@ function closedBodyTransform(schema: any): Record<string, string> | null {
|
|
|
1972
1689
|
return null
|
|
1973
1690
|
}
|
|
1974
1691
|
|
|
1975
|
-
// Null prototype: a spec is free to declare a property called `__proto__`,
|
|
1976
|
-
// and on an ordinary object that assignment sets the prototype instead of
|
|
1977
|
-
// creating an own property — the mapping would vanish, and with it the
|
|
1978
|
-
// whole body restriction if it were the only one.
|
|
1979
1692
|
const out: Record<string, string> = Object.create(null)
|
|
1980
1693
|
for (const name of names) {
|
|
1981
1694
|
out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`'
|
|
@@ -1983,16 +1696,6 @@ function closedBodyTransform(schema: any): Record<string, string> | null {
|
|
|
1983
1696
|
return out
|
|
1984
1697
|
}
|
|
1985
1698
|
|
|
1986
|
-
// The first sentence of `text` (up to a `.`/`!`/`?` followed by whitespace
|
|
1987
|
-
// or end), whitespace-collapsed and length-capped with an ellipsis.
|
|
1988
|
-
//
|
|
1989
|
-
// Shared by the API summary and by a field's `short`, because both answer
|
|
1990
|
-
// the same question — give me one line of prose for a place that has room
|
|
1991
|
-
// for one line. A field's `short` is interpolated straight into a markdown
|
|
1992
|
-
// table cell by every generated Readme, where a raw newline ends the row and
|
|
1993
|
-
// takes the rest of the table with it. Specs supply plenty: bullet lists,
|
|
1994
|
-
// fenced examples and multi-paragraph notes all appear as a property's
|
|
1995
|
-
// `description`, up to 1725 characters of it in the validation corpus.
|
|
1996
1699
|
function firstSentence(text: string): string {
|
|
1997
1700
|
const collapsed = text.replace(/\s+/g, ' ').trim()
|
|
1998
1701
|
const m = collapsed.match(/^(.+?[.!?])(\s|$)/)
|
|
@@ -2037,6 +1740,7 @@ export {
|
|
|
2037
1740
|
normalizeFieldName,
|
|
2038
1741
|
prefixLeadingDigit,
|
|
2039
1742
|
debugpath,
|
|
1743
|
+
debugpathOn,
|
|
2040
1744
|
findPathsWithPrefix,
|
|
2041
1745
|
writeFileSyncWarn,
|
|
2042
1746
|
warnOnError,
|