@voxgig/apidef 8.2.3 → 8.3.0
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/desc.d.ts +2 -0
- package/dist/guide/graphql01.d.ts +3 -2
- package/dist/guide/graphql01.js +48 -2
- package/dist/guide/graphql01.js.map +1 -1
- package/dist/guide/heuristic01.js +119 -5
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/model.d.ts +4 -0
- package/dist/transform/args.js +21 -2
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/entity.js +1 -0
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +42 -0
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.js +43 -3
- package/dist/transform/operation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +32 -8
- package/dist/utility.js.map +1 -1
- package/model/apidef.aon +22 -0
- package/model/guide.aon +5 -0
- package/package.json +1 -1
- package/src/desc.ts +6 -0
- package/src/guide/graphql01.ts +61 -3
- package/src/guide/heuristic01.ts +134 -4
- package/src/model.ts +17 -0
- package/src/transform/args.ts +26 -2
- package/src/transform/entity.ts +1 -0
- package/src/transform/field.ts +45 -0
- package/src/transform/operation.ts +49 -3
- package/src/utility.ts +34 -9
package/src/transform/args.ts
CHANGED
|
@@ -71,7 +71,7 @@ const argsTransform: Transform = async function(
|
|
|
71
71
|
argdefs.push(...(opdef?.parameters ?? []))
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
resolveArgs(ment, mop, mpoint, argdefs)
|
|
74
|
+
resolveArgs(ctx, ment, mop, mpoint, argdefs)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
})
|
|
@@ -116,7 +116,10 @@ const ARG_KIND: Record<string, ModelArg["kind"]> = {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
function resolveArgs(
|
|
119
|
+
function resolveArgs(
|
|
120
|
+
ctx: any,
|
|
121
|
+
ment: ModelEntity, mop: ModelOp, mpoint: ModelPoint, argdefs: ParameterDef[]
|
|
122
|
+
) {
|
|
120
123
|
const touchedKeys = new Set<string>()
|
|
121
124
|
|
|
122
125
|
each(argdefs, (argdef: ParameterDef) => {
|
|
@@ -124,6 +127,27 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mpoint: ModelPoint, argdef
|
|
|
124
127
|
// by; the snakified form is the user-friendly runtime identifier.
|
|
125
128
|
const specName = normalizeFieldName(argdef.name)
|
|
126
129
|
const orig = depluralize(snakify(specName))
|
|
130
|
+
|
|
131
|
+
// A parameter with no name is not a parameter. This is what a DANGLING
|
|
132
|
+
// `$ref` looks like by the time it reaches here: the reference survives
|
|
133
|
+
// unresolved, `name` and `in` are both absent, and the arg would become
|
|
134
|
+
// a nameless `query` entry that every target then has to render. Ruby
|
|
135
|
+
// cannot: `Struct.new(:"")` raises at load and takes the whole SDK with
|
|
136
|
+
// it. Drop it and say which reference is missing.
|
|
137
|
+
if ('' === orig) {
|
|
138
|
+
const ref = (argdef as any)?.$ref
|
|
139
|
+
ctx?.warn?.({
|
|
140
|
+
note: `Parameter with no name on entity=${ment.name} op=${mop.name}` +
|
|
141
|
+
` path=${mpoint.orig} is dropped` +
|
|
142
|
+
(null == ref ? '.' : `: \`$ref\` "${ref}" resolves to nothing.`) +
|
|
143
|
+
' A parameter needs a `name`, or a reference that resolves to one.',
|
|
144
|
+
entity: ment.name,
|
|
145
|
+
path: mpoint.orig,
|
|
146
|
+
op: mop.name,
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
const kind = ARG_KIND[argdef.in] ?? 'query'
|
|
128
152
|
// Rename map can be keyed by either the spec original (camelCase) or by
|
|
129
153
|
// the snakified form depending on which path went through heuristic01.
|
package/src/transform/entity.ts
CHANGED
package/src/transform/field.ts
CHANGED
|
@@ -182,6 +182,34 @@ function resolveOpFields(
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// SPEC FACTS ABOUT THE FIELD, carried through verbatim.
|
|
186
|
+
//
|
|
187
|
+
// These four are declared by OpenAPI on the property and were being
|
|
188
|
+
// dropped on the floor. `readOnly` is the one that matters most: it is
|
|
189
|
+
// the difference between a field a client MAY send and one it may not,
|
|
190
|
+
// and nothing else in the model says which — so every generator has been
|
|
191
|
+
// putting server-assigned fields into the type a caller fills in.
|
|
192
|
+
//
|
|
193
|
+
// ONLY WHEN THE SPEC SAYS SO, and for the booleans only when TRUE. Each
|
|
194
|
+
// defaults to false in OpenAPI, so an absent key and an explicit `false`
|
|
195
|
+
// carry the same information; emitting the false ones would add a key to
|
|
196
|
+
// every field of every model and say nothing. Same discipline as
|
|
197
|
+
// `short`: absent means "the spec did not say", never "apidef dropped
|
|
198
|
+
// it".
|
|
199
|
+
for (const flag of ['readOnly', 'writeOnly', 'deprecated'] as const) {
|
|
200
|
+
if (true === (fielddef as any)[flag]) {
|
|
201
|
+
mfield[flag] = true
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// `format` is an open vocabulary — OpenAPI defines a handful and lets a
|
|
206
|
+
// spec coin its own — so it is carried as the string it is rather than
|
|
207
|
+
// interpreted here. `password` is the one a generator acts on today.
|
|
208
|
+
const ffmt = (fielddef as any).format
|
|
209
|
+
if ('string' === typeof ffmt && '' !== ffmt.trim()) {
|
|
210
|
+
mfield.format = ffmt.trim()
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
// Record an untagged union under this field. The field is already typed
|
|
186
214
|
// openly ($ANY/$ARRAY/$OBJECT) because there is nothing to narrow it to;
|
|
187
215
|
// this says WHY, so the generated docs can explain the open type instead
|
|
@@ -536,6 +564,23 @@ function mergeField(
|
|
|
536
564
|
existingField.short = newField.short
|
|
537
565
|
}
|
|
538
566
|
|
|
567
|
+
// The spec facts merge the same way, and for the same reason: one schema
|
|
568
|
+
// annotates the field and another references it bare, so taking the first
|
|
569
|
+
// declaration in opFieldPrecedence order is what finds the annotation.
|
|
570
|
+
//
|
|
571
|
+
// THE PRECEDENCE ORDER PUTS `load` FIRST, WHICH IS THE SAFE DIRECTION HERE.
|
|
572
|
+
// A field the response schema marks readOnly and a request body also lists
|
|
573
|
+
// is a self-contradictory spec — OpenAPI says a client must not send a
|
|
574
|
+
// readOnly property at all — and this resolves it by believing the
|
|
575
|
+
// restriction rather than the omission. Marking a writable field readOnly
|
|
576
|
+
// costs a caller one field; the other way round sends a value the server
|
|
577
|
+
// rejects.
|
|
578
|
+
for (const flag of ['readOnly', 'writeOnly', 'deprecated', 'format'] as const) {
|
|
579
|
+
if (null == existingField[flag] && null != newField[flag]) {
|
|
580
|
+
(existingField as any)[flag] = newField[flag]
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
539
584
|
return existingField
|
|
540
585
|
}
|
|
541
586
|
|
|
@@ -25,6 +25,18 @@ import type {
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
// The op names the transform resolves. Anything else under a guide path's
|
|
29
|
+
// `op` map is dropped, and an unknown name (a verb such as `merge`, or a
|
|
30
|
+
// typo) is dropped WITH A WARNING: guide.aon is the only correction surface
|
|
31
|
+
// (ADR-002), so a correction that vanishes silently defeats it. A non-CRUD
|
|
32
|
+
// verb is declared as `action: <verb>: {}` beside a CRUD op on the same path.
|
|
33
|
+
const RESOLVED_OPS = ['load', 'list', 'create', 'update', 'remove', 'patch']
|
|
34
|
+
|
|
35
|
+
// Emitted by the heuristic for HEAD and OPTIONS methods; no SDK operation
|
|
36
|
+
// exists for them yet, so they are skipped without a warning.
|
|
37
|
+
const IGNORED_OPS = ['head', 'options', 'OPTIONS']
|
|
38
|
+
|
|
39
|
+
|
|
28
40
|
const operationTransform: Transform = async function(
|
|
29
41
|
ctx: any,
|
|
30
42
|
): Promise<TransformResult> {
|
|
@@ -36,7 +48,7 @@ const operationTransform: Transform = async function(
|
|
|
36
48
|
each(guide.entity, (gent: GuideEntity, entname: string) => {
|
|
37
49
|
if (!guideActive(gent)) return
|
|
38
50
|
|
|
39
|
-
collectOps(gent)
|
|
51
|
+
collectOps(ctx, gent)
|
|
40
52
|
|
|
41
53
|
const opm: ModelOpMap = {
|
|
42
54
|
load: undefined,
|
|
@@ -63,7 +75,7 @@ const operationTransform: Transform = async function(
|
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
|
|
66
|
-
function collectOps(gent: GuideEntity) {
|
|
78
|
+
function collectOps(ctx: any, gent: GuideEntity) {
|
|
67
79
|
; (gent as any).opm$ = (gent as any).opm$ ?? {}
|
|
68
80
|
each((gent as any).paths$, (pathdesc: PathDesc) => {
|
|
69
81
|
each(pathdesc.op, (gop: GuidePathOp, opname: OpName) => {
|
|
@@ -72,6 +84,20 @@ function collectOps(gent: GuideEntity) {
|
|
|
72
84
|
return
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
if (!RESOLVED_OPS.includes(opname)) {
|
|
88
|
+
if (!IGNORED_OPS.includes(opname)) {
|
|
89
|
+
ctx.warn?.({
|
|
90
|
+
note: `Unknown op "${opname}" on entity=${gent.name} path=${pathdesc.orig}` +
|
|
91
|
+
` is dropped: only ${RESOLVED_OPS.join('/')} are resolved.` +
|
|
92
|
+
` Declare a verb as \`action: ${opname}: {}\` beside a CRUD op on that path.`,
|
|
93
|
+
entity: gent.name,
|
|
94
|
+
path: pathdesc.orig,
|
|
95
|
+
op: opname,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
; (gent as any).opm$[opname] = (gent as any).opm$[opname] ?? { paths: [] }
|
|
76
102
|
|
|
77
103
|
const oppathdesc: PathDesc = {
|
|
@@ -80,6 +106,7 @@ function collectOps(gent: GuideEntity) {
|
|
|
80
106
|
rename: pathdesc.rename,
|
|
81
107
|
method: gop.method as any,
|
|
82
108
|
op: gop as any,
|
|
109
|
+
action: pathdesc.action,
|
|
83
110
|
def: pathdesc.def,
|
|
84
111
|
}
|
|
85
112
|
|
|
@@ -126,7 +153,18 @@ function resolvePatch(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp {
|
|
|
126
153
|
const opdesc = resolveOp('patch', gent)
|
|
127
154
|
|
|
128
155
|
// If patch is actually update, make it update!
|
|
129
|
-
|
|
156
|
+
//
|
|
157
|
+
// That holds when there is no PUT update at all, and equally when every
|
|
158
|
+
// PUT update point is an ACTION: a verb such as GitHub's `merge` borrows
|
|
159
|
+
// the update slot (actions have no slot of their own) but is not the
|
|
160
|
+
// entity's update. Leaving PATCH as `patch` there made the real update
|
|
161
|
+
// unreachable, since no target emits a `patch` method, and routed a plain
|
|
162
|
+
// update() to the verb. The action points join the promoted PATCH, and
|
|
163
|
+
// `$action` selects them at call time.
|
|
164
|
+
if (null != opdesc && (null == opm.update || onlyActionPaths(gent, 'update'))) {
|
|
165
|
+
if (null != opm.update) {
|
|
166
|
+
opdesc.points.push(...opm.update.points)
|
|
167
|
+
}
|
|
130
168
|
opm.update = opdesc
|
|
131
169
|
opm.update.name = 'update'
|
|
132
170
|
}
|
|
@@ -138,6 +176,14 @@ function resolvePatch(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp {
|
|
|
138
176
|
}
|
|
139
177
|
|
|
140
178
|
|
|
179
|
+
// True when every path collected under the op carries a guide action.
|
|
180
|
+
function onlyActionPaths(gent: GuideEntity, opname: OpName): boolean {
|
|
181
|
+
const paths: PathDesc[] = (gent as any).opm$?.[opname]?.paths ?? []
|
|
182
|
+
return 0 < paths.length &&
|
|
183
|
+
paths.every((p: PathDesc) => 0 < Object.keys(p.action ?? {}).length)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
141
187
|
function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
|
|
142
188
|
let mop: undefined | ModelOp = undefined
|
|
143
189
|
let opdesc = (gent as any).opm$[opname]
|
package/src/utility.ts
CHANGED
|
@@ -1070,6 +1070,37 @@ function canonizeCmpName(orig: string): string {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
|
|
1073
|
+
const FIRST_LETTER_RE = /[a-zA-Z]/
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
// No target language permits an identifier that starts with a digit, so a
|
|
1077
|
+
// name derived from one — a `3dsSession` schema, a `/2fa` path segment, a
|
|
1078
|
+
// `_3DSecure` GraphQL type — is prefixed with an `n`.
|
|
1079
|
+
//
|
|
1080
|
+
// The prefix takes the case of the name it guards: lower for `3ds_session`,
|
|
1081
|
+
// upper for `3DSecure`. That keeps the result inside whatever casing
|
|
1082
|
+
// convention the caller was already working in, so a later PascalCase or
|
|
1083
|
+
// camelCase conversion has nothing to undo. A name with no letter in it at
|
|
1084
|
+
// all (`404`) takes the lower-case prefix.
|
|
1085
|
+
//
|
|
1086
|
+
// This is the ONE place the rule lives. Entity names reach it through
|
|
1087
|
+
// `ensureMinEntityName` and the GraphQL guide's `entityName`; project slugs
|
|
1088
|
+
// through `sanitizeSlug`.
|
|
1089
|
+
//
|
|
1090
|
+
// FIELD names deliberately do NOT come here. A field name is a WIRE
|
|
1091
|
+
// identifier and renaming it makes the SDK read a key the server never sends
|
|
1092
|
+
// — the mistake `canonizeField` exists to document. Targets escape those at
|
|
1093
|
+
// the point of emission instead.
|
|
1094
|
+
function prefixLeadingDigit(s: string): string {
|
|
1095
|
+
if (null == s || '' === s) return s
|
|
1096
|
+
const first = s.charCodeAt(0)
|
|
1097
|
+
if (first < 48 || first > 57) return s
|
|
1098
|
+
const letter = s.match(FIRST_LETTER_RE)
|
|
1099
|
+
const upper = null != letter && letter[0] >= 'A' && letter[0] <= 'Z'
|
|
1100
|
+
return (upper ? 'N' : 'n') + s
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
|
|
1073
1104
|
// Sanitize a raw slug into a clean kebab-case string suitable for
|
|
1074
1105
|
// conversion to a valid JS identifier (via camelify/snakify/etc).
|
|
1075
1106
|
function sanitizeSlug(s: string): string {
|
|
@@ -1098,12 +1129,7 @@ function sanitizeSlug(s: string): string {
|
|
|
1098
1129
|
|
|
1099
1130
|
if (!out) return 'unknown'
|
|
1100
1131
|
|
|
1101
|
-
|
|
1102
|
-
if (/^\d/.test(out)) {
|
|
1103
|
-
out = 'n' + out
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return out
|
|
1132
|
+
return prefixLeadingDigit(out)
|
|
1107
1133
|
}
|
|
1108
1134
|
|
|
1109
1135
|
|
|
@@ -1172,9 +1198,7 @@ function ensureMinEntityName(
|
|
|
1172
1198
|
padded = truncated || parts[0].substring(0, MAX_ENTITY_NAME_LEN)
|
|
1173
1199
|
}
|
|
1174
1200
|
|
|
1175
|
-
|
|
1176
|
-
padded = 'n' + padded
|
|
1177
|
-
}
|
|
1201
|
+
padded = prefixLeadingDigit(padded)
|
|
1178
1202
|
if (padded.length < MIN_ENTITY_NAME_LEN) {
|
|
1179
1203
|
const padding = 'nt'.substring(0, MIN_ENTITY_NAME_LEN - padded.length)
|
|
1180
1204
|
padded = padded + padding
|
|
@@ -1956,6 +1980,7 @@ export {
|
|
|
1956
1980
|
ensureMinEntityName,
|
|
1957
1981
|
inferFieldType,
|
|
1958
1982
|
normalizeFieldName,
|
|
1983
|
+
prefixLeadingDigit,
|
|
1959
1984
|
debugpath,
|
|
1960
1985
|
findPathsWithPrefix,
|
|
1961
1986
|
writeFileSyncWarn,
|