@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
|
@@ -11,23 +11,6 @@ import {
|
|
|
11
11
|
} from '../utility'
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
// Detect a path-parameter that is in fact the entity's own id, after URL
|
|
15
|
-
// renaming. Three ways an identity can show up:
|
|
16
|
-
// 1. The param literally has name 'id' (the common case for e.g. /things/{id}).
|
|
17
|
-
// 2. The param's lower-camelCase name appears in `point.rename.param` mapping
|
|
18
|
-
// to 'id' — e.g. `{connectionId: 'id'}` for `/companies/{company_id}/connections/{id}`.
|
|
19
|
-
// In this case the param's own name is `connection_id` (apidef snake-cased
|
|
20
|
-
// it), which doesn't equal 'id' but the segment's `var` is 'id'.
|
|
21
|
-
// 3. Positional convention: for singleton ops (load/update/remove), the
|
|
22
|
-
// LAST variable segment in the path is the entity's own id. Catches
|
|
23
|
-
// cases where the entity name and path placeholder differ in spelling
|
|
24
|
-
// (e.g. entity `enviroment` vs path `/environments/{environment_id}`)
|
|
25
|
-
// and apidef therefore didn't synthesize a rename-to-id.
|
|
26
|
-
//
|
|
27
|
-
// Without this helper, the flow generator double-counts the entity's id —
|
|
28
|
-
// emitting it as both `srcdatavar.id` AND a separate body field — which the
|
|
29
|
-
// in-memory test mock then requires to match a non-existent field on the
|
|
30
|
-
// stored entity.
|
|
31
14
|
function isEntityIdParam(point: any, param: any, opname?: string): boolean {
|
|
32
15
|
if ('id' === param?.name) return true
|
|
33
16
|
const renameMap = point?.rename?.param
|
|
@@ -79,8 +62,6 @@ const flowstepTransform: Transform = async function(
|
|
|
79
62
|
const ent: ModelEntity = kit.entity[flow.entity]
|
|
80
63
|
const opmap = ent.op
|
|
81
64
|
|
|
82
|
-
// TODO: spec parameter passed into each step func, used semantically by generator
|
|
83
|
-
// validation: part of spec, semantic name and params, up to generator how to use it
|
|
84
65
|
const ref01 = ent.name + '_ref01'
|
|
85
66
|
|
|
86
67
|
createStep(opmap, flow, ent, { input: { ref: ref01 } })
|
|
@@ -186,12 +167,6 @@ const createStep: MakeFlowStep = (
|
|
|
186
167
|
|
|
187
168
|
each(point.args.params, (param: any) => {
|
|
188
169
|
if ('id' === param.name) {
|
|
189
|
-
// For CREATE, `id` in the path is NOT the entity's own id (entity is
|
|
190
|
-
// being created here — its id doesn't exist yet). It's some parent's
|
|
191
|
-
// id renamed by apidef's path normalization (e.g. `space_id` → `id`
|
|
192
|
-
// in `/spaces/{id}/space_memberships` for SpaceMembership). Recover
|
|
193
|
-
// the original snake_case name so the test seeds the parent's id
|
|
194
|
-
// into both the created entity's data AND the URL.
|
|
195
170
|
const origName = originalSnakeNameOfRenamedId(point)
|
|
196
171
|
if (origName) {
|
|
197
172
|
step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
|
|
@@ -203,12 +178,6 @@ const createStep: MakeFlowStep = (
|
|
|
203
178
|
step.match[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
|
|
204
179
|
})
|
|
205
180
|
|
|
206
|
-
// Also seed any path-param fields required by other ops (typically LIST
|
|
207
|
-
// through a sibling parent path), so the in-memory test mock can find
|
|
208
|
-
// the just-created entity when a later step queries by those fields.
|
|
209
|
-
// Without this, a metric created at /pages/{page_id}/metrics/data lacks
|
|
210
|
-
// the page_access_user_id field required by
|
|
211
|
-
// /pages/{page_id}/page_access_users/{page_access_user_id}/metrics LIST.
|
|
212
181
|
seedRelatedOpParams(opmap, point, step)
|
|
213
182
|
|
|
214
183
|
flow.step.push(step)
|
|
@@ -227,8 +196,6 @@ function seedRelatedOpParams(opmap: any, createPoint: any, step: ModelEntityFlow
|
|
|
227
196
|
if (!param?.name) continue
|
|
228
197
|
if (isEntityIdParam(point, param, opname as any)) continue
|
|
229
198
|
if (step.match[param.name] !== undefined) continue
|
|
230
|
-
// For renamed-from-id params on CREATE's chosen point we'd already
|
|
231
|
-
// have set the snake-case origin; don't double-write.
|
|
232
199
|
if ('id' === param.name) continue
|
|
233
200
|
step.match[param.name] =
|
|
234
201
|
param.name.replace(/_id/, '') + '01'
|
|
@@ -251,10 +218,6 @@ const listStep: MakeFlowStep = (
|
|
|
251
218
|
|
|
252
219
|
each(point.args.params, (param: any) => {
|
|
253
220
|
if ('id' === param.name) {
|
|
254
|
-
// For LIST, `id` in the path is a parent's id renamed by apidef
|
|
255
|
-
// (LIST doesn't address a single entity by id). Recover the original
|
|
256
|
-
// snake_case name so test code references a real idmap entry rather
|
|
257
|
-
// than landing on the bogus `id01` default.
|
|
258
221
|
const origName = originalSnakeNameOfRenamedId(point)
|
|
259
222
|
if (origName) {
|
|
260
223
|
step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
|
|
@@ -348,18 +311,6 @@ const removeStep: MakeFlowStep = (
|
|
|
348
311
|
}
|
|
349
312
|
|
|
350
313
|
|
|
351
|
-
// The field the generated basic-flow test WRITES, to prove an update took
|
|
352
|
-
// effect. It must not be a route parameter of the update itself.
|
|
353
|
-
//
|
|
354
|
-
// The test mock builds its lookup selector out of the request data, so when
|
|
355
|
-
// the marked field is also a path segment the selector asks for the record by
|
|
356
|
-
// the value it is about to write — it matches nothing and the mock answers
|
|
357
|
-
// 404, on a flow that is doing exactly what it was told to.
|
|
358
|
-
//
|
|
359
|
-
// trello's notification_channel_setting chose `channel`, the {channel} of
|
|
360
|
-
// PUT /members/{member_id}/notificationsChannelSettings/{channel}, and its js,
|
|
361
|
-
// php and ts suites all failed on it. Updating a routing key is a rename, not
|
|
362
|
-
// a field update, and neither the mock nor the SDK models it as one.
|
|
363
314
|
function firstTextField(ent: ModelEntity, op?: ModelOp) {
|
|
364
315
|
const paramNames: Record<string, boolean> = {}
|
|
365
316
|
each((op as any)?.points).forEach((pt: any) => {
|
package/src/transform/graphql.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
/* Copyright (c) 2024-2026 Voxgig, MIT License */
|
|
2
2
|
|
|
3
|
-
// Render the GraphQL wire data onto each point: the complete operation
|
|
4
|
-
// document, its variable bindings, the response unwrap path, and (for list
|
|
5
|
-
// ops) the pagination descriptor.
|
|
6
|
-
//
|
|
7
|
-
// Documents are computed HERE, once, and stored in the model as strings.
|
|
8
|
-
// The alternative — shipping structured selection data and assembling query
|
|
9
|
-
// text inside every generated SDK — would mean one query assembler per
|
|
10
|
-
// language target, all of which must stay semantically identical. One
|
|
11
|
-
// renderer in apidef is the whole reason GraphQL support stays affordable
|
|
12
|
-
// across the target matrix.
|
|
13
|
-
//
|
|
14
|
-
// Documents are rendered SINGLE-LINE with sorted selection fields, so the
|
|
15
|
-
// emitted model is byte-stable and schema drift shows up in model diffs.
|
|
16
3
|
|
|
17
4
|
import { each } from 'jostraca'
|
|
18
5
|
|
|
@@ -33,9 +20,6 @@ import type {
|
|
|
33
20
|
import { deriveRetShape } from '../guide/graphql01'
|
|
34
21
|
|
|
35
22
|
|
|
36
|
-
// Fields the default fragment never selects on a to-one relation: the stub
|
|
37
|
-
// carries the id only, so the caller loads the related entity through its
|
|
38
|
-
// own entity op.
|
|
39
23
|
const REL_STUB = '{ id }'
|
|
40
24
|
|
|
41
25
|
|
|
@@ -108,9 +92,6 @@ function payloadScalarFields(typeName: string, def: any): string[] {
|
|
|
108
92
|
}
|
|
109
93
|
|
|
110
94
|
|
|
111
|
-
// Variable bindings for a root field: one per argument. `from` is the op
|
|
112
|
-
// argument the value is read from; for the input-object argument that is the
|
|
113
|
-
// request data itself.
|
|
114
95
|
function buildVars(fielddef: any, def: any): ModelGraphqlVar[] {
|
|
115
96
|
const args = fielddef?.args ?? []
|
|
116
97
|
|
|
@@ -139,7 +120,6 @@ function buildVars(fielddef: any, def: any): ModelGraphqlVar[] {
|
|
|
139
120
|
}
|
|
140
121
|
|
|
141
122
|
|
|
142
|
-
// `issue(id: $id, first: $first)` — argument list wired to variables.
|
|
143
123
|
function argList(vars: ModelGraphqlVar[]): string {
|
|
144
124
|
return 0 === vars.length ? '' :
|
|
145
125
|
'(' + vars.map((v) => v.name + ': $' + v.name).join(', ') + ')'
|
|
@@ -158,7 +138,6 @@ function varDecl(vars: ModelGraphqlVar[]): string {
|
|
|
158
138
|
}
|
|
159
139
|
|
|
160
140
|
|
|
161
|
-
// Render one operation document, single-line.
|
|
162
141
|
function renderDoc(
|
|
163
142
|
opname: string,
|
|
164
143
|
optype: string,
|
|
@@ -176,7 +155,6 @@ function renderDoc(
|
|
|
176
155
|
' fragment ' + fragName + ' on ' + fragType +
|
|
177
156
|
' { ' + fragFields.join(' ') + ' }' : '')
|
|
178
157
|
|
|
179
|
-
// Collapse any accidental double spacing so the string is canonical.
|
|
180
158
|
return doc.replace(/\s+/g, ' ').trim()
|
|
181
159
|
}
|
|
182
160
|
|
|
@@ -220,7 +198,6 @@ const graphqlTransform: Transform = async function(
|
|
|
220
198
|
|
|
221
199
|
const vars = buildVars(fielddef, def)
|
|
222
200
|
|
|
223
|
-
// Selection shape and response unwrap both follow the return kind.
|
|
224
201
|
let selection = fragSpread
|
|
225
202
|
let respath = 'body.data.' + rootfield
|
|
226
203
|
|
|
@@ -235,12 +212,6 @@ const graphqlTransform: Transform = async function(
|
|
|
235
212
|
selection = fragSpread
|
|
236
213
|
}
|
|
237
214
|
else if ('payload' === ret.kind && null == ret.entity) {
|
|
238
|
-
// Entity-less payload: Linear's DeletePayload is entityId +
|
|
239
|
-
// success + lastSyncId and nothing else. The classifier admits
|
|
240
|
-
// these (the entity comes from the field name), so the renderer
|
|
241
|
-
// must not fall through to the default `{ id }` spread — the
|
|
242
|
-
// payload HAS no id, and the server rejects the whole document.
|
|
243
|
-
// Select the payload's own scalars and unwrap to the payload.
|
|
244
215
|
const own = payloadScalarFields(fielddef.type, def)
|
|
245
216
|
selection = '{ ' + (0 < own.length ? own.join(' ') : '__typename') + ' }'
|
|
246
217
|
respath = 'body.data.' + rootfield
|
|
@@ -256,17 +227,10 @@ const graphqlTransform: Transform = async function(
|
|
|
256
227
|
respath = 'body.data.' + rootfield + '.' + ret.unwrap
|
|
257
228
|
}
|
|
258
229
|
|
|
259
|
-
// Distinct operation name per point. The action comes from the GUIDE,
|
|
260
|
-
// not from mpoint.select: selectTransform runs after this stage, so
|
|
261
|
-
// $action is not set yet, and without the suffix every action point
|
|
262
|
-
// on an op would ship the same operation name (three PlanetUpdates),
|
|
263
|
-
// which is what server logs, tracing and APM key on.
|
|
264
230
|
const actionName = Object.keys((gfield as any)?.action ?? {})[0]
|
|
265
231
|
const docname = pascal(entname) + pascal(opname) +
|
|
266
232
|
(null != actionName ? pascal(actionName) : '')
|
|
267
233
|
|
|
268
|
-
// GraphQL points ride the HTTP machinery: POST to the single
|
|
269
|
-
// endpoint, no path segments. The document carries everything else.
|
|
270
234
|
mpoint.kind = 'graphql'
|
|
271
235
|
mpoint.method = 'POST'
|
|
272
236
|
mpoint.segments = []
|
|
@@ -79,7 +79,6 @@ function collectOps(ctx: any, gent: GuideEntity) {
|
|
|
79
79
|
; (gent as any).opm$ = (gent as any).opm$ ?? {}
|
|
80
80
|
each((gent as any).paths$, (pathdesc: PathDesc) => {
|
|
81
81
|
each(pathdesc.op, (gop: GuidePathOp, opname: OpName) => {
|
|
82
|
-
// Op-level opt-out; see the entity-level note in transform/entity.ts.
|
|
83
82
|
if (!guideActive(gop)) {
|
|
84
83
|
return
|
|
85
84
|
}
|
|
@@ -152,15 +151,6 @@ function resolveRemove(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp
|
|
|
152
151
|
function resolvePatch(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp {
|
|
153
152
|
const opdesc = resolveOp('patch', gent)
|
|
154
153
|
|
|
155
|
-
// If patch is actually update, make it update!
|
|
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
154
|
if (null != opdesc && (null == opm.update || onlyActionPaths(gent, 'update'))) {
|
|
165
155
|
if (null != opm.update) {
|
|
166
156
|
opdesc.points.push(...opm.update.points)
|
|
@@ -191,13 +181,6 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
|
|
|
191
181
|
mop = {
|
|
192
182
|
name: opname,
|
|
193
183
|
points: opdesc.paths.map((p: PathDesc) => {
|
|
194
|
-
// Renames already applied by entity.ts resolvePathList — re-applying
|
|
195
|
-
// here corrupted paths for any spec where rename map maps an old
|
|
196
|
-
// name to a value that another rename maps to a different new name
|
|
197
|
-
// (e.g. gitlab `/groups/{id}/badges/{badge_id}` with rename
|
|
198
|
-
// `{badge_id: 'id', id: 'project_id'}` ended up as
|
|
199
|
-
// `/groups/{project_id}/badges/{project_id}` — the second pass
|
|
200
|
-
// rewrote the freshly-renamed `{id}` into `{project_id}` again).
|
|
201
184
|
const segments = p.segments
|
|
202
185
|
|
|
203
186
|
const mpoint: ModelPoint = {
|
|
@@ -206,12 +189,6 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
|
|
|
206
189
|
rename: p.rename,
|
|
207
190
|
method: p.method,
|
|
208
191
|
args: {},
|
|
209
|
-
// Carry the per-path op transform (res `body.<entity>`, req
|
|
210
|
-
// `{<entity>: reqdata}`) computed by the guide step
|
|
211
|
-
// (heuristic01 ResolveTransform) onto the point. It lives on the
|
|
212
|
-
// path's op, not on the op-map entry, so read p.op.transform.
|
|
213
|
-
// Spread into a fresh object so the default-fill below never
|
|
214
|
-
// mutates the shared guide op.transform across points.
|
|
215
192
|
transform: { ...((p as any).op?.transform ?? {}) },
|
|
216
193
|
select: {
|
|
217
194
|
exist: []
|
package/src/transform/top.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
} from '../types'
|
|
15
15
|
|
|
16
16
|
import type {
|
|
17
|
-
// GuidePath,
|
|
18
17
|
PathDesc,
|
|
19
18
|
OpDesc,
|
|
20
19
|
} from '../desc'
|
|
@@ -24,19 +23,9 @@ import type {
|
|
|
24
23
|
} from '../model'
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
// Guide* => from guide model
|
|
28
|
-
// *Desc => internal working descriptiuon
|
|
29
|
-
// *Def => API spec definition
|
|
30
|
-
// Model* => Generated SDK Model
|
|
31
26
|
|
|
32
27
|
|
|
33
|
-
// type GuideEntity = {
|
|
34
|
-
// name: string,
|
|
35
|
-
// path: Record<string, GuidePath>
|
|
36
28
|
|
|
37
|
-
// paths$: PathDesc[]
|
|
38
|
-
// opm$: Record<OpName, OpDesc>
|
|
39
|
-
// }
|
|
40
29
|
|
|
41
30
|
|
|
42
31
|
const topTransform = async function(
|
|
@@ -55,14 +44,6 @@ const topTransform = async function(
|
|
|
55
44
|
// the docs generated from it) never carry an empty/degenerate description.
|
|
56
45
|
kit.info.description = ensureDescription(kit.info)
|
|
57
46
|
|
|
58
|
-
// Public APIs that declare NO authentication (no security schemes, no
|
|
59
|
-
// top-level `security`, and no per-operation `security`) get an explicit
|
|
60
|
-
// no-auth signal in the model. Downstream sdkgen reads it via
|
|
61
|
-
// isAuthActive() (main.kit.info.auth === false) to suppress apikey/auth
|
|
62
|
-
// code, docs and examples. Only the negative signal is emitted: when the
|
|
63
|
-
// spec DOES declare auth we leave `auth` unset so the SDK's own config
|
|
64
|
-
// (main.kit.config.auth) governs. Set AFTER stringifyInfoScalars so the
|
|
65
|
-
// value stays a real boolean rather than the string "false".
|
|
66
47
|
if (true === def.graphql) {
|
|
67
48
|
// A GraphQL schema NEVER declares HTTP auth, so specDeclaresAuth would
|
|
68
49
|
// report every secured GraphQL API (Linear included) as public and
|
|
@@ -80,8 +61,6 @@ const topTransform = async function(
|
|
|
80
61
|
type: authopt.type ?? 'apiKey',
|
|
81
62
|
in: authopt.in ?? 'header',
|
|
82
63
|
name: authopt.name ?? 'Authorization',
|
|
83
|
-
// '' means a raw credential with no prefix (Linear's style);
|
|
84
|
-
// prepareAuth in generated SDKs already honours that.
|
|
85
64
|
prefix: authopt.prefix ?? '',
|
|
86
65
|
}
|
|
87
66
|
}
|
|
@@ -98,12 +77,6 @@ const topTransform = async function(
|
|
|
98
77
|
if (null != security) {
|
|
99
78
|
kit.info.security = security
|
|
100
79
|
|
|
101
|
-
// Record the ACCESS-TOKEN EXCHANGE, when the spec describes one. The
|
|
102
|
-
// endpoint that issues credentials is not a resource and does not
|
|
103
|
-
// become an entity (the guide deactivates it — see
|
|
104
|
-
// guide/heuristic01.ts and ADR-002); these facts are how it survives
|
|
105
|
-
// into the model instead, and they are exactly what sdkgen's
|
|
106
|
-
// `secrets` feature needs to drive the exchange.
|
|
107
80
|
const exchange = findAuthExchange(def)
|
|
108
81
|
if (null != exchange) {
|
|
109
82
|
kit.info.security.exchange = exchange
|
|
@@ -111,7 +84,6 @@ const topTransform = async function(
|
|
|
111
84
|
}
|
|
112
85
|
}
|
|
113
86
|
|
|
114
|
-
// Swagger 2.0
|
|
115
87
|
if (def.host) {
|
|
116
88
|
kit.info.servers.push({
|
|
117
89
|
url: (def.schemes?.[0] ?? 'https') + '://' + join([def.host, def.basePath], '/', true)
|
|
@@ -127,25 +99,15 @@ const topTransform = async function(
|
|
|
127
99
|
if (!server || 'string' !== typeof server.url) continue
|
|
128
100
|
const url: string = server.url.trim()
|
|
129
101
|
if (url === '') continue
|
|
130
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) continue
|
|
131
|
-
// `//host/path` is a protocol-relative URL — meaningless to a
|
|
132
|
-
// backend SDK, treat as missing-scheme and default to https.
|
|
102
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) continue
|
|
133
103
|
if (url.startsWith('//')) {
|
|
134
104
|
server.url = 'https:' + url
|
|
135
105
|
continue
|
|
136
106
|
}
|
|
137
|
-
// `/path` is path-only (relative to wherever the spec is served).
|
|
138
|
-
// Leave it untouched; it's a valid OpenAPI form.
|
|
139
107
|
if (url.startsWith('/')) continue
|
|
140
108
|
server.url = 'https://' + url
|
|
141
109
|
}
|
|
142
110
|
|
|
143
|
-
// A usable SDK requires a base URL. OpenAPI 3 puts it in `servers[].url`;
|
|
144
|
-
// Swagger 2 derives it from `host` + `basePath`. If neither yields a
|
|
145
|
-
// non-empty url, the generated SDK has no way to issue requests, so fail
|
|
146
|
-
// the apidef model build rather than emit broken code.
|
|
147
|
-
// (For GraphQL the parser already synthesised servers[0] from the required
|
|
148
|
-
// `endpoint` build option, so this check passes on the same terms.)
|
|
149
111
|
const firstServerUrl: any = kit.info.servers?.[0]?.url
|
|
150
112
|
if (null == firstServerUrl || '' === String(firstServerUrl).trim()) {
|
|
151
113
|
throw new Error(
|
|
@@ -156,8 +118,6 @@ const topTransform = async function(
|
|
|
156
118
|
)
|
|
157
119
|
}
|
|
158
120
|
|
|
159
|
-
// A short "what this API is" blurb and a canonical website link, for doc
|
|
160
|
-
// generators. Only set when derivable, so downstream can gate on them.
|
|
161
121
|
const summary = resolveSummary(def)
|
|
162
122
|
if (null != summary) {
|
|
163
123
|
kit.info.summary = summary
|
|
@@ -171,8 +131,6 @@ const topTransform = async function(
|
|
|
171
131
|
}
|
|
172
132
|
|
|
173
133
|
|
|
174
|
-
// True when the text carries at least one alphabetic character — i.e. it is
|
|
175
|
-
// real prose rather than a placeholder like "." / "---" / whitespace.
|
|
176
134
|
function hasLetters(text: string): boolean {
|
|
177
135
|
return /[a-zA-Z]/.test(text)
|
|
178
136
|
}
|
|
@@ -210,22 +168,18 @@ function resolveSummary(def: any): string | undefined {
|
|
|
210
168
|
}
|
|
211
169
|
|
|
212
170
|
const desc = 'string' === typeof info.description ? info.description : ''
|
|
213
|
-
// Treat letterless prose (a bare "." placeholder, "---", …) as no summary
|
|
214
|
-
// rather than surfacing it verbatim.
|
|
215
171
|
if ('' === desc.trim() || !hasLetters(desc)) {
|
|
216
172
|
return undefined
|
|
217
173
|
}
|
|
218
174
|
|
|
219
175
|
const lines = desc.split('\n')
|
|
220
176
|
let i = 0
|
|
221
|
-
// Skip leading blank lines, ATX headings (`# ...`) and setext underlines.
|
|
222
177
|
while (i < lines.length &&
|
|
223
178
|
('' === lines[i].trim() ||
|
|
224
179
|
/^\s*#{1,6}\s/.test(lines[i]) ||
|
|
225
180
|
/^\s*(-{2,}|={2,})\s*$/.test(lines[i]))) {
|
|
226
181
|
i++
|
|
227
182
|
}
|
|
228
|
-
// Take the first paragraph (up to the next blank line or heading).
|
|
229
183
|
const para: string[] = []
|
|
230
184
|
while (i < lines.length &&
|
|
231
185
|
'' !== lines[i].trim() &&
|
|
@@ -238,12 +192,6 @@ function resolveSummary(def: any): string | undefined {
|
|
|
238
192
|
}
|
|
239
193
|
|
|
240
194
|
|
|
241
|
-
// A canonical link back to the API's own website, in priority order:
|
|
242
|
-
// 1. externalDocs.url (the spec's explicit external link)
|
|
243
|
-
// 2. info['x-logo'].href (redoc homepage link)
|
|
244
|
-
// 3. homepage from the server (strip an api./developer./docs. subdomain)
|
|
245
|
-
// 4. info.contact.url
|
|
246
|
-
// 5. info.termsOfService
|
|
247
195
|
function resolveWebsite(def: any, servers: any[]): string | undefined {
|
|
248
196
|
const info = def?.info ?? {}
|
|
249
197
|
|
|
@@ -272,15 +220,6 @@ function homepageFromServer(url: any): string | undefined {
|
|
|
272
220
|
const u = new URL(url.includes('://') ? url : 'https://' + url)
|
|
273
221
|
let host = u.hostname
|
|
274
222
|
if ('' === host || !host.includes('.')) return undefined
|
|
275
|
-
// A templated server — `https://{instance}.dreamapply.com/api`, a per-tenant
|
|
276
|
-
// API declaring `instance` as an OpenAPI server variable — is a legitimate
|
|
277
|
-
// spec, but its host is not a resolvable address. Deriving a "website" from
|
|
278
|
-
// it put `https://{instance}.dreamapply.com` into the README as a clickable
|
|
279
|
-
// link that no browser can open. The URL parser accepts the braces, so the
|
|
280
|
-
// placeholder has to be rejected explicitly.
|
|
281
|
-
//
|
|
282
|
-
// Only the WEBSITE is skipped: the server URL itself is still emitted, where
|
|
283
|
-
// the variable is meaningful and the runtime substitutes it.
|
|
284
223
|
if (/[{}]/.test(host)) return undefined
|
|
285
224
|
host = host.replace(
|
|
286
225
|
/^(api|api-[a-z0-9]+|apis|developer|developers|docs?|www)\./i, '')
|
|
@@ -297,24 +236,6 @@ function isHttpUrl(v: any): boolean {
|
|
|
297
236
|
}
|
|
298
237
|
|
|
299
238
|
|
|
300
|
-
// Describe the spec's PRIMARY security scheme as model facts
|
|
301
|
-
// (info.security): scheme key, type, where the credential goes (in/name),
|
|
302
|
-
// and the value prefix for Authorization-header credentials. The primary
|
|
303
|
-
// scheme is the one named by the first top-level `security` requirement,
|
|
304
|
-
// falling back to the first declared scheme. Returns null when nothing
|
|
305
|
-
// usable is declared (the no-auth signal is handled separately).
|
|
306
|
-
//
|
|
307
|
-
// Prefix rules:
|
|
308
|
-
// http basic/bearer -> 'Basic' / 'Bearer'
|
|
309
|
-
// oauth2 / openIdConnect -> 'Bearer' (access token in Authorization)
|
|
310
|
-
// apiKey in an Authorization header -> the prefix the API's own prose
|
|
311
|
-
// documents (e.g. `Authorization: OAuth <key>`), else '' (raw). An
|
|
312
|
-
// `apiKey` scheme means "send the credential as-is" — a `Bearer`/etc.
|
|
313
|
-
// prefix is only implied by an `http`+`bearer` scheme or explicit
|
|
314
|
-
// prose, so absent evidence the key goes in raw (e.g. The SMS Works'
|
|
315
|
-
// `Authorization: <jwt>`). A user override is available via
|
|
316
|
-
// config.auth.prefix.
|
|
317
|
-
// apiKey in any other header/query/cookie -> '' (raw credential)
|
|
318
239
|
function resolveSecurity(def: any): Record<string, string> | null {
|
|
319
240
|
const schemes: Record<string, any> =
|
|
320
241
|
def.components?.securitySchemes ?? def.securityDefinitions ?? {}
|
|
@@ -344,8 +265,6 @@ function resolveSecurity(def: any): Record<string, string> | null {
|
|
|
344
265
|
}
|
|
345
266
|
|
|
346
267
|
if ('http' === type) {
|
|
347
|
-
// Swagger 2 `type: basic` has no `scheme`; OpenAPI 3 uses
|
|
348
|
-
// `scheme: basic|bearer|...`.
|
|
349
268
|
out.prefix = 'basic' === String(scheme.scheme ?? '').toLowerCase() ?
|
|
350
269
|
'Basic' : 'Bearer'
|
|
351
270
|
}
|
|
@@ -367,25 +286,12 @@ function resolveSecurity(def: any): Record<string, string> | null {
|
|
|
367
286
|
findAuthPrefix(def.info?.description) ??
|
|
368
287
|
''
|
|
369
288
|
}
|
|
370
|
-
// else: raw credential in a named header/query/cookie — no prefix.
|
|
371
289
|
}
|
|
372
290
|
|
|
373
291
|
return out
|
|
374
292
|
}
|
|
375
293
|
|
|
376
294
|
|
|
377
|
-
// Find the spec's access-token exchange and describe it as model facts:
|
|
378
|
-
// where it lives, and the field names it sends and answers with. Returns
|
|
379
|
-
// null when the spec describes no exchange, which is the common case.
|
|
380
|
-
//
|
|
381
|
-
// `path` is RELATIVE to the server URL, with no leading slash, because the
|
|
382
|
-
// server URL already carries whatever account or tenant segment the API
|
|
383
|
-
// templates into it — an absolute path would drop that segment. sdkgen's
|
|
384
|
-
// secrets feature resolves it against `options.base` for the same reason.
|
|
385
|
-
//
|
|
386
|
-
// Only the FIRST exchange is recorded. A spec describing two token
|
|
387
|
-
// endpoints is describing two auth schemes, which is a bigger thing than a
|
|
388
|
-
// field on info.security and is not guessed at here.
|
|
389
295
|
function findAuthExchange(def: any): Record<string, string> | null {
|
|
390
296
|
const secured = specSecuredByDefault(def)
|
|
391
297
|
if (!secured) {
|
|
@@ -418,18 +324,6 @@ function findAuthExchange(def: any): Record<string, string> | null {
|
|
|
418
324
|
}
|
|
419
325
|
|
|
420
326
|
|
|
421
|
-
// Extract the credential prefix from a securityScheme's / info prose.
|
|
422
|
-
// Three signals, in confidence order:
|
|
423
|
-
// 1. An explicit `Authorization: <prefix> <cred>` line (any prefix word)
|
|
424
|
-
// — e.g. Statuspage's `Authorization: OAuth 89a2...`. The prefix must
|
|
425
|
-
// be a short word followed by something credential-shaped (a long
|
|
426
|
-
// token, or a `<key>` / `{token}` / `$KEY` / `YOUR_...` placeholder),
|
|
427
|
-
// so a bare `Authorization: 89a2...` doesn't match.
|
|
428
|
-
// 2. A KNOWN scheme word (Bearer/OAuth/Token/Basic) shown as an example
|
|
429
|
-
// prefix — `Example: Bearer eyJ...` (NoFrixion's shape).
|
|
430
|
-
// 3. A KNOWN scheme word named as the scheme — `the Bearer scheme`,
|
|
431
|
-
// `Bearer authentication`.
|
|
432
|
-
// Returns null when nothing indicates a prefix (an apiKey then goes in raw).
|
|
433
327
|
function findAuthPrefix(text: unknown): string | null {
|
|
434
328
|
if ('string' !== typeof text || '' === text) {
|
|
435
329
|
return null
|
|
@@ -441,14 +335,12 @@ function findAuthPrefix(text: unknown): string | null {
|
|
|
441
335
|
return explicit[1]
|
|
442
336
|
}
|
|
443
337
|
|
|
444
|
-
// A known scheme word as an example prefix, then a credential-shaped tail.
|
|
445
338
|
const example = text.match(
|
|
446
339
|
/(?:example|e\.g\.)[:\s][^\n]{0,20}?\b(Bearer|OAuth2?|Token|Basic)\b[ \t]+(?:<[^>\n]+>|\{[^}\n]+\}|[A-Za-z0-9._~+/=-]{6,})/i)
|
|
447
340
|
if (null != example) {
|
|
448
341
|
return canonAuthScheme(example[1])
|
|
449
342
|
}
|
|
450
343
|
|
|
451
|
-
// A known scheme word named as the auth scheme.
|
|
452
344
|
const named = text.match(
|
|
453
345
|
/\b(Bearer|OAuth2?|Token|Basic)\b[ \t]+(?:scheme|authentication|auth\b|credentials?)/i)
|
|
454
346
|
if (null != named) {
|
|
@@ -459,7 +351,6 @@ function findAuthPrefix(text: unknown): string | null {
|
|
|
459
351
|
}
|
|
460
352
|
|
|
461
353
|
|
|
462
|
-
// Canonical casing for a known scheme word (Bearer/OAuth/Token/Basic).
|
|
463
354
|
function canonAuthScheme(word: string): string {
|
|
464
355
|
const w = word.toLowerCase()
|
|
465
356
|
if (w.startsWith('oauth')) return 'OAuth'
|
|
@@ -470,27 +361,18 @@ function canonAuthScheme(word: string): string {
|
|
|
470
361
|
}
|
|
471
362
|
|
|
472
363
|
|
|
473
|
-
// Does the spec declare any authentication? True if it defines security
|
|
474
|
-
// schemes (OpenAPI 3 `components.securitySchemes` or Swagger 2
|
|
475
|
-
// `securityDefinitions`), a top-level `security` requirement, or a
|
|
476
|
-
// per-operation `security` requirement. Used to emit a no-auth signal
|
|
477
|
-
// (info.auth: false) for fully public APIs.
|
|
478
364
|
function specDeclaresAuth(def: any): boolean {
|
|
479
365
|
if (null == def || 'object' !== typeof def) return false
|
|
480
366
|
|
|
481
367
|
const nonEmptyObj = (v: any) =>
|
|
482
368
|
null != v && 'object' === typeof v && Object.keys(v).length > 0
|
|
483
369
|
|
|
484
|
-
// OpenAPI 3 security schemes.
|
|
485
370
|
if (nonEmptyObj(def.components?.securitySchemes)) return true
|
|
486
371
|
|
|
487
|
-
// Swagger 2 security definitions.
|
|
488
372
|
if (nonEmptyObj(def.securityDefinitions)) return true
|
|
489
373
|
|
|
490
|
-
// Top-level security requirement.
|
|
491
374
|
if (Array.isArray(def.security) && def.security.length > 0) return true
|
|
492
375
|
|
|
493
|
-
// Per-operation security requirement.
|
|
494
376
|
const paths = def.paths
|
|
495
377
|
if (paths && 'object' === typeof paths) {
|
|
496
378
|
for (const pathItem of Object.values(paths)) {
|
|
@@ -508,13 +390,6 @@ function specDeclaresAuth(def: any): boolean {
|
|
|
508
390
|
}
|
|
509
391
|
|
|
510
392
|
|
|
511
|
-
// OpenAPI's `info` object (and the `servers` array) declares every scalar
|
|
512
|
-
// leaf as a string. YAML/JSON parsers don't enforce that — `version: 2`
|
|
513
|
-
// without quotes parses as the number 2, `version: true` as a boolean.
|
|
514
|
-
// Apidef's downstream schema (apidef.aontu) unifies info fields as
|
|
515
|
-
// `string`, so non-string scalars cause an aontu unify failure during
|
|
516
|
-
// model resolution. Normalise scalar leaves to strings here, at the
|
|
517
|
-
// model-build boundary, rather than relax the schema.
|
|
518
393
|
function stringifyInfoScalars(node: any): any {
|
|
519
394
|
if (null == node) return node
|
|
520
395
|
if (Array.isArray(node)) return node.map(stringifyInfoScalars)
|
|
@@ -541,8 +416,3 @@ export {
|
|
|
541
416
|
homepageFromServer,
|
|
542
417
|
findAuthPrefix,
|
|
543
418
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// export type {
|
|
547
|
-
// GuideEntity,
|
|
548
|
-
// }
|
package/src/transform.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { operationTransform } from './transform/operation'
|
|
|
13
13
|
import { argsTransform } from './transform/args'
|
|
14
14
|
import { selectTransform } from './transform/select'
|
|
15
15
|
import { fieldTransform } from './transform/field'
|
|
16
|
-
// import { manualTransform } from './transform/manual'
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ import * as Fs from 'node:fs'
|
|
|
5
5
|
import { Pino, prettyPino } from '@voxgig/util'
|
|
6
6
|
import { Shape, Open, Any } from 'shape'
|
|
7
7
|
|
|
8
|
+
import type { ResolvedSpec } from './resolved'
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
const KIT = 'kit'
|
|
10
12
|
|
|
@@ -50,7 +52,6 @@ type ApiDefOptions = {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
// Input definition format.
|
|
54
55
|
type DefKind = 'OpenAPI' | 'GraphQL'
|
|
55
56
|
|
|
56
57
|
|
|
@@ -208,6 +209,8 @@ type ApiDefContext = {
|
|
|
208
209
|
warn: any,
|
|
209
210
|
metrics: Metrics,
|
|
210
211
|
work: Record<string, any>
|
|
212
|
+
|
|
213
|
+
resolved?: ResolvedSpec
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
|
|
@@ -237,7 +240,6 @@ type GuideControl = {}
|
|
|
237
240
|
type GuideMetrics = {
|
|
238
241
|
count: {
|
|
239
242
|
path: number
|
|
240
|
-
// Schema root fields classified (GraphQL guides; 0 for OpenAPI).
|
|
241
243
|
field: number
|
|
242
244
|
method: number
|
|
243
245
|
entity: number
|
|
@@ -253,15 +255,6 @@ type GuideMetrics = {
|
|
|
253
255
|
|
|
254
256
|
|
|
255
257
|
type GuideEntity = {
|
|
256
|
-
// COMPOSITE IDENTITY CORRECTION, the guide's say over how an entity is
|
|
257
|
-
// addressed. apidef infers a compound key from adjacent path parameters,
|
|
258
|
-
// which is right far more often than not and cannot always be right:
|
|
259
|
-
// `/…/artifacts/{artifact_id}/{archive_format}` reads as composite and is
|
|
260
|
-
// not. Stating it here is the documented correction surface (ADR-002).
|
|
261
|
-
//
|
|
262
|
-
// `parts` names the compound key outright; `composite: false` says these
|
|
263
|
-
// adjacent parameters are not one (the record still has a key); `sep`
|
|
264
|
-
// changes the separator without restating the parts.
|
|
265
258
|
id?: {
|
|
266
259
|
parts?: string[]
|
|
267
260
|
sep?: string
|
|
@@ -281,8 +274,6 @@ type GuideEntity = {
|
|
|
281
274
|
// Why the heuristic deactivated it, so guide.aon reads as a record of a
|
|
282
275
|
// decision rather than an unexplained `active: false`.
|
|
283
276
|
why_inactive?: string
|
|
284
|
-
// GraphQL guides key operations by schema root field instead of path;
|
|
285
|
-
// the two branches are mutually exclusive per guide.
|
|
286
277
|
field?: Record<string, GuidePath>
|
|
287
278
|
path: Record<string, GuidePath>
|
|
288
279
|
}
|