@voxgig/apidef 8.10.0 → 8.12.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/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 +19 -352
- 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 +4 -4
- 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 +28 -353
- 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/transform/field.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { TransformResult, Transform } from '../transform'
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
validator, canonizeField, inferFieldType, normalizeFieldName, envelopeProp,
|
|
9
|
+
canonizeCmpName,
|
|
9
10
|
scanUntaggedUnion, firstSentence,
|
|
10
11
|
} from '../utility'
|
|
11
12
|
|
|
@@ -49,24 +50,6 @@ const fieldTransform: Transform = async function(
|
|
|
49
50
|
const mpoints = mop.points
|
|
50
51
|
|
|
51
52
|
for (let mpoint of mpoints) {
|
|
52
|
-
// ACTION POINTS CONTRIBUTE NO FIELDS, as `identityParams` and
|
|
53
|
-
// `responseCandidates` below already assume: an action is a VERB
|
|
54
|
-
// dispatched by `$action`, so its request body is that verb's
|
|
55
|
-
// arguments and its response is that verb's result. Neither says
|
|
56
|
-
// anything about what a record of this entity carries.
|
|
57
|
-
//
|
|
58
|
-
// A custom action lands under `create`, so every point of it was
|
|
59
|
-
// harvested along with the plain create's. solar's planet — four
|
|
60
|
-
// properties in the spec — came out with ten fields, the extra six
|
|
61
|
-
// being `{start, stop}` and `{forbid, why}` from the two action
|
|
62
|
-
// bodies and `{ok, state}` from their shared response envelope.
|
|
63
|
-
// Those reached the generated `Planet` type, its create and update
|
|
64
|
-
// data types, and the per-entity field table in the generated
|
|
65
|
-
// reference, none of which a planet has ever carried.
|
|
66
|
-
if (null != mpoint?.select?.['$action']) {
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
|
|
70
53
|
const opfields = resolveOpFields(ment, mop, mpoint, def)
|
|
71
54
|
|
|
72
55
|
for (let opfield of opfields) {
|
|
@@ -86,42 +69,11 @@ const fieldTransform: Transform = async function(
|
|
|
86
69
|
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
87
70
|
})
|
|
88
71
|
|
|
89
|
-
// Mark the entity as having an id only when the spec actually declares one.
|
|
90
|
-
// Downstream (test generators, fixture builders) gate id-specific code on
|
|
91
|
-
// this presence so that public read-only APIs without ids don't get
|
|
92
|
-
// bogus id assertions.
|
|
93
|
-
// COMPOSITE FIRST, because a compound key need not come with an `id`.
|
|
94
|
-
//
|
|
95
|
-
// An entity addressed by `{owner}/{repo}` whose response carries only
|
|
96
|
-
// `owner` and `name` has no field literally named `id`, and its adjacent
|
|
97
|
-
// placeholders are left unrenamed so `addressedById` is false too.
|
|
98
|
-
// Neither branch below then ran, so the entity got NO id descriptor and
|
|
99
|
-
// even an explicit `guide.entity.<name>.id.parts` was silently ignored —
|
|
100
|
-
// while the Go port, which initialises a descriptor unconditionally,
|
|
101
|
-
// emitted the composite. The ports disagreed on exactly the shape this
|
|
102
|
-
// feature exists for.
|
|
103
72
|
const gent = guide?.entity?.[ment.name]
|
|
104
73
|
const composite = compositeId(ment, gent, def)
|
|
105
74
|
|
|
106
75
|
const idField = fields.find((f: ModelField) => 'id' === f.name)
|
|
107
76
|
|
|
108
|
-
// A COMPOSITE ID IS A STRING, whatever the API's own `id` field is —
|
|
109
|
-
// AND THE API'S OWN id IS KEPT.
|
|
110
|
-
//
|
|
111
|
-
// github's repo declares `id` as an integer, its global database id,
|
|
112
|
-
// while the composite identity is `owner/repo`. Two facts have to
|
|
113
|
-
// survive: `id` must hold a string, because that is what the joined
|
|
114
|
-
// value is and what every generated type has to store; and the spec's
|
|
115
|
-
// numeric property must not be silently reinterpreted, because a
|
|
116
|
-
// consumer that wants the database id is entitled to it with its own
|
|
117
|
-
// type and format intact.
|
|
118
|
-
//
|
|
119
|
-
// So the API's field MOVES to `<api>_id` rather than being rewritten in
|
|
120
|
-
// place, carrying its type, format and per-op overrides with it, and the
|
|
121
|
-
// entity's `alias.field` map records where it went. Retyping in place
|
|
122
|
-
// (the first attempt) claimed the server's numeric id was a string;
|
|
123
|
-
// leaving it alone made `id.field` name a declaration the runtime value
|
|
124
|
-
// cannot satisfy. Moving it is the only option that lies about neither.
|
|
125
77
|
if (null != composite.parts && null != idField && !scalarStringField(idField)) {
|
|
126
78
|
const idf: any = idField
|
|
127
79
|
const apiname = String((model as any)?.name || 'api')
|
|
@@ -187,12 +139,6 @@ const fieldTransform: Transform = async function(
|
|
|
187
139
|
ment.id = { name: 'id', field: 'id', ...composite }
|
|
188
140
|
}
|
|
189
141
|
else if (addressedById(ment)) {
|
|
190
|
-
// The FIELD as well as the descriptor. An entity addressed by id has an
|
|
191
|
-
// id at runtime — the test fixture seeds one, and the SDK sends it — so
|
|
192
|
-
// a model that declares the descriptor without the field makes the
|
|
193
|
-
// generated TYPE disagree with the generated TEST: trello's Option,
|
|
194
|
-
// Reaction and Sticker compiled to `TS2339: Property 'id' does not
|
|
195
|
-
// exist` the moment the test started assigning data.id.
|
|
196
142
|
fields.push({
|
|
197
143
|
name: 'id',
|
|
198
144
|
type: '`$STRING`',
|
|
@@ -201,26 +147,6 @@ const fieldTransform: Transform = async function(
|
|
|
201
147
|
fields.sort((a: ModelField, b: ModelField) =>
|
|
202
148
|
a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
203
149
|
|
|
204
|
-
// ADDRESSABLE BY ID WITHOUT DECLARING ONE AS A FIELD.
|
|
205
|
-
//
|
|
206
|
-
// The rule above reads the RESPONSE schema, and plenty of real entities
|
|
207
|
-
// are addressed by an id their response never repeats. github's
|
|
208
|
-
// private_registry is one: PATCH /orgs/{org}/private-registries/{secret_name}
|
|
209
|
-
// renames secret_name to id, so the entity is addressed by id on every
|
|
210
|
-
// one of its own routes, while its schema declares only created_at, key,
|
|
211
|
-
// name, url and friends.
|
|
212
|
-
//
|
|
213
|
-
// Downstream that absence is not cosmetic. TestEntity gates
|
|
214
|
-
// `data.id = <created>.id` on THIS descriptor, so the generated update
|
|
215
|
-
// carried no id at all, the test mock's selector fell back to whatever
|
|
216
|
-
// else was in reqdata (org_id), matched no single record, and the flow
|
|
217
|
-
// failed with a 404 that named nothing to do with ids.
|
|
218
|
-
//
|
|
219
|
-
// An entity whose own points take an `id` param IS addressable by id;
|
|
220
|
-
// that is the property the downstream generators actually want. Entities
|
|
221
|
-
// with neither a field nor an id param — the read-only public APIs the
|
|
222
|
-
// rule above was written for — still get no descriptor, so they still
|
|
223
|
-
// get no id assertions.
|
|
224
150
|
ment.id = { name: 'id', field: 'id', ...composite }
|
|
225
151
|
}
|
|
226
152
|
|
|
@@ -232,14 +158,6 @@ const fieldTransform: Transform = async function(
|
|
|
232
158
|
|
|
233
159
|
|
|
234
160
|
|
|
235
|
-
// The separator that joins a composite id into one string.
|
|
236
|
-
//
|
|
237
|
-
// A forward slash cannot occur inside a single path segment — a raw `/`
|
|
238
|
-
// would end the segment, and a value that legitimately contains one arrives
|
|
239
|
-
// percent-encoded as `%2F` — so joining on it can never be ambiguous, and
|
|
240
|
-
// splitting on it can never over-split. That is what makes the composite id
|
|
241
|
-
// safe to carry as a single opaque string, which is the property the SDK and
|
|
242
|
-
// Seneca entities are built on.
|
|
243
161
|
const ID_SEP = '/'
|
|
244
162
|
|
|
245
163
|
// Subfields that conventionally carry the identifying value of a nested
|
|
@@ -255,36 +173,6 @@ const NESTED_ID_KEYS = ['login', 'slug', 'name', 'key', 'id']
|
|
|
255
173
|
const ID_OPS = ['load', 'update', 'patch', 'remove']
|
|
256
174
|
|
|
257
175
|
|
|
258
|
-
// The parameters that TOGETHER name one record: the trailing run of
|
|
259
|
-
// ADJACENT variable segments on the addressing route.
|
|
260
|
-
//
|
|
261
|
-
// ADJACENCY IS THE WHOLE TEST, and it is what separates a compound key from
|
|
262
|
-
// ordinary parent/child nesting:
|
|
263
|
-
//
|
|
264
|
-
// /repos/{owner}/{repo} -> owner, repo COMPOSITE
|
|
265
|
-
// /api/planet/{planet_id}/moon/{moon_id} -> moon_id single
|
|
266
|
-
// /repos/{owner}/{repo}/pulls/{pull_number} -> pull_number single
|
|
267
|
-
//
|
|
268
|
-
// A literal segment between two variables names a SUB-COLLECTION, so the
|
|
269
|
-
// earlier variable scopes the later one — `planet_id` says which planet's
|
|
270
|
-
// moons, and `moon_id` alone identifies the moon. Two variables with nothing
|
|
271
|
-
// between them address no sub-collection: neither value names anything on
|
|
272
|
-
// its own, and only the pair identifies a repository.
|
|
273
|
-
//
|
|
274
|
-
// Taking every variable on the path instead was tried first and is wrong on
|
|
275
|
-
// most real specs — it made `moon` (planet_id + moon_id), petstore's `order`,
|
|
276
|
-
// `pet` and `user`, and taxonomy's `domain` and `kingdom` all falsely
|
|
277
|
-
// composite, which the apidef-validate goldens caught immediately. Nested
|
|
278
|
-
// resources are the common shape; compound keys are the exception, and
|
|
279
|
-
// adjacency is the thing that actually distinguishes them.
|
|
280
|
-
//
|
|
281
|
-
// Read from the op that names a single record, never from `list`: a
|
|
282
|
-
// collection route's path params are the entity's parents. A point ending in
|
|
283
|
-
// a literal is a verb ON the record (`.../{number}/merge`) and carries the
|
|
284
|
-
// same variables, so it is a fallback rather than a different answer.
|
|
285
|
-
// Walk back from a point's end, collecting variables until a literal stops
|
|
286
|
-
// the run. That literal is the sub-collection boundary; anything before it
|
|
287
|
-
// scopes this record rather than naming it.
|
|
288
176
|
function trailingVars(point: any): string[] {
|
|
289
177
|
const segs = ((point?.segments || []) as any[]).filter((s: any) => null != s)
|
|
290
178
|
const run: string[] = []
|
|
@@ -301,15 +189,6 @@ function trailingVars(point: any): string[] {
|
|
|
301
189
|
|
|
302
190
|
|
|
303
191
|
function identityParams(ment: ModelEntity): string[] {
|
|
304
|
-
// EVERY ID-BEARING OP AT ONCE, not the first one that offers a candidate.
|
|
305
|
-
//
|
|
306
|
-
// These four ops all address a single record, so all four describe the
|
|
307
|
-
// same identity — but they do not all carry the same routes. gitlab's
|
|
308
|
-
// `project` has `/api/v4/projects/{id}` under `remove` alone, while its
|
|
309
|
-
// `load` carries only sub-resources like
|
|
310
|
-
// `/api/v4/projects/{id}/uploads/{secret}/{filename}`. Returning on the
|
|
311
|
-
// first op with any candidate therefore made a PROJECT identified by
|
|
312
|
-
// `secret/filename`. The op order is now only a tie-break.
|
|
313
192
|
const cands: any[] = []
|
|
314
193
|
|
|
315
194
|
for (let o = 0; o < ID_OPS.length; o++) {
|
|
@@ -331,22 +210,6 @@ function identityParams(ment: ModelEntity): string[] {
|
|
|
331
210
|
run,
|
|
332
211
|
// Segments BEFORE the run: how much parent scope the route needs.
|
|
333
212
|
scope: ((pt.segments || []).length - run.length),
|
|
334
|
-
// DOES THE RUN END IN THE RECORD'S OWN KEY? Then it is the
|
|
335
|
-
// record's address and nothing further is needed.
|
|
336
|
-
//
|
|
337
|
-
// This transform RENAMES that parameter to `id`, so a run ending in
|
|
338
|
-
// it is this port's own statement of what identifies the record —
|
|
339
|
-
// and the composite inference must not contradict it.
|
|
340
|
-
// `/gists/{gist_id}` becomes `/gists/{id}` and is a gist;
|
|
341
|
-
// `/gists/{gist_id}/{sha}` is a REVISION of one, and won on key
|
|
342
|
-
// length alone, so a gist came out keyed `gist_id/sha` while the
|
|
343
|
-
// generated SDK's own load match takes the single parameter. The
|
|
344
|
-
// same contradiction gave cloudsmith's repo and vulnerability
|
|
345
|
-
// compound keys their SDKs never address them by.
|
|
346
|
-
//
|
|
347
|
-
// Deliberately narrow: exactly `id` or an unrenamed `<entity>_id`,
|
|
348
|
-
// never any `*_id`. `actor_type/actor_id` IS a compound key, and a
|
|
349
|
-
// looser test breaks it.
|
|
350
213
|
own: 'id' === run[run.length - 1] ||
|
|
351
214
|
(ment as any).name + '_id' === run[run.length - 1],
|
|
352
215
|
order: o,
|
|
@@ -354,34 +217,6 @@ function identityParams(ment: ModelEntity): string[] {
|
|
|
354
217
|
}
|
|
355
218
|
}
|
|
356
219
|
|
|
357
|
-
// WHICH ROUTE IS THE RECORD'S OWN ADDRESS.
|
|
358
|
-
//
|
|
359
|
-
// An entity gathers every route that reads it, and in a large
|
|
360
|
-
// specification most of those are sub-resources. Three earlier rules were
|
|
361
|
-
// measured against the validation corpus, and each is wrong:
|
|
362
|
-
//
|
|
363
|
-
// The FIRST route listed gave github's `repo` the single part
|
|
364
|
-
// `subject_digest`, from
|
|
365
|
-
// `/repos/{owner}/{repo}/attestations/{subject_digest}` — no compound
|
|
366
|
-
// key at all, for the entity this feature exists for. Invisible on a
|
|
367
|
-
// small spec, where the first item route IS the record's own.
|
|
368
|
-
//
|
|
369
|
-
// The SHORTEST route ending in a variable took cloudsmith's
|
|
370
|
-
// `/vulnerabilities/{owner}/` — a LIST of an owner's vulnerabilities —
|
|
371
|
-
// and cut a four-part key down to `owner`, dropping three more
|
|
372
|
-
// composites. Ending in a variable does not make a route an address.
|
|
373
|
-
//
|
|
374
|
-
// The LONGEST trailing run took
|
|
375
|
-
// `/orgs/{org}/teams/{team_slug}/repos/{owner}/{repo}` and made a TEAM
|
|
376
|
-
// identified by `owner/repo`. A deep sub-resource can carry more
|
|
377
|
-
// adjacent variables than the record's own route does.
|
|
378
|
-
//
|
|
379
|
-
// What separates them is PARENT SCOPE: the record's own route is the
|
|
380
|
-
// least-qualified one that names it, and among equally-qualified routes
|
|
381
|
-
// the one carrying the fullest key. `/repos/{owner}/{repo}` is qualified
|
|
382
|
-
// by one segment and the attestations route by four; `/teams/{team_id}`
|
|
383
|
-
// by one and the org-team-repo route by five; cloudsmith's vulnerability
|
|
384
|
-
// routes are all qualified by one, so the fullest of them wins.
|
|
385
220
|
const best = cands.reduce((b: any, c: any) => {
|
|
386
221
|
if (null == b) {
|
|
387
222
|
return c
|
|
@@ -401,34 +236,6 @@ function identityParams(ment: ModelEntity): string[] {
|
|
|
401
236
|
return null == best ? [] : best.run
|
|
402
237
|
}
|
|
403
238
|
|
|
404
|
-
// THE PROPERTY MAPS A RESPONSE COULD BE DESCRIBING, best first.
|
|
405
|
-
//
|
|
406
|
-
// BOTH SPEC DIALECTS. An OpenAPI 3 response carries its schema under
|
|
407
|
-
// `content['application/json']`; a SWAGGER 2 response carries it directly as
|
|
408
|
-
// `schema`. Reading only the first resolved nothing for every Swagger 2 spec
|
|
409
|
-
// in the validation corpus.
|
|
410
|
-
//
|
|
411
|
-
// JSON ONLY, where there is a choice. An operation may declare several media
|
|
412
|
-
// types with different schemas, and field extraction uses the JSON one — so
|
|
413
|
-
// picking whichever came first in source order could infer a path from an XML
|
|
414
|
-
// or binary schema that the actual JSON record does not have.
|
|
415
|
-
//
|
|
416
|
-
// `allOf` IS EXPANDED, because a response that composes its entity that way
|
|
417
|
-
// has neither `properties` nor `items` of its own. field extraction expands
|
|
418
|
-
// it; not doing so here meant the fields were present while the id could not
|
|
419
|
-
// be reconstructed.
|
|
420
|
-
//
|
|
421
|
-
// ONLY THE ENVELOPE IS DESCENDED, via the same `envelopeProp` rule field
|
|
422
|
-
// extraction uses. Descending every object-valued property instead treats an
|
|
423
|
-
// ordinary nested object as a whole record: for `{ slug, metadata: { tenant } }`
|
|
424
|
-
// addressed by `{tenant}/{slug}`, `tenant` resolved to `tenant` rather than
|
|
425
|
-
// `metadata.tenant` — a confidently wrong path, which is worse than no
|
|
426
|
-
// mapping at all.
|
|
427
|
-
//
|
|
428
|
-
// ACTION POINTS ARE SKIPPED, as `identityParams` skips them: an action's
|
|
429
|
-
// response is a verb's result, not a representation of the entity, so a field
|
|
430
|
-
// that happens to appear there says nothing about what a returned record
|
|
431
|
-
// carries.
|
|
432
239
|
function responseCandidates(ment: ModelEntity, def: any): any[] {
|
|
433
240
|
const out: any[] = []
|
|
434
241
|
const seen = new Set<any>()
|
|
@@ -509,7 +316,6 @@ function responseCandidates(ment: ModelEntity, def: any): any[] {
|
|
|
509
316
|
}
|
|
510
317
|
}
|
|
511
318
|
|
|
512
|
-
// Swagger 2 puts it here.
|
|
513
319
|
add(resdef.schema, opname)
|
|
514
320
|
}
|
|
515
321
|
}
|
|
@@ -519,6 +325,19 @@ function responseCandidates(ment: ModelEntity, def: any): any[] {
|
|
|
519
325
|
}
|
|
520
326
|
|
|
521
327
|
|
|
328
|
+
|
|
329
|
+
function namesEntity(schema: any, ment: ModelEntity): boolean {
|
|
330
|
+
const xref = schema?.['x-ref']
|
|
331
|
+
|
|
332
|
+
if ('string' !== typeof xref) {
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const cmp = xref.slice(xref.lastIndexOf('/') + 1)
|
|
337
|
+
|
|
338
|
+
return canonizeCmpName(cmp) === ment.name
|
|
339
|
+
}
|
|
340
|
+
|
|
522
341
|
// A `$ref` followed one hop, or the schema itself. apidef resolves most refs
|
|
523
342
|
// before this stage; this covers the ones that survive on a nested property.
|
|
524
343
|
function resolveRef(schema: any, def: any): any {
|
|
@@ -583,12 +402,6 @@ function partAliases(ment: ModelEntity, part: string): string[] {
|
|
|
583
402
|
}
|
|
584
403
|
|
|
585
404
|
|
|
586
|
-
// Where one part is carried in a given property map, or null.
|
|
587
|
-
//
|
|
588
|
-
// The four rules, in order, each a fact the spec states: a scalar property of
|
|
589
|
-
// that name; the part naming this entity, resolved to `name`; a scalar
|
|
590
|
-
// `<part>_name` / `_login` / `_slug`; or an object property's conventional
|
|
591
|
-
// identifying subfield.
|
|
592
405
|
function resolvePart(
|
|
593
406
|
ment: ModelEntity,
|
|
594
407
|
part: string,
|
|
@@ -637,52 +450,15 @@ function resolvePart(
|
|
|
637
450
|
}
|
|
638
451
|
|
|
639
452
|
|
|
640
|
-
// WHERE EACH COMPOSITE PART'S VALUE LIVES IN A RESPONSE.
|
|
641
|
-
//
|
|
642
|
-
// The parts are PATH PARAMETER names; a response names its fields whatever it
|
|
643
|
-
// likes. Resolving one to the other is what lets an SDK put an id on a record
|
|
644
|
-
// the API returned, rather than only address a record whose id it was given.
|
|
645
|
-
//
|
|
646
|
-
// The rules, in order, and each of them is a fact about the spec rather than
|
|
647
|
-
// a guess:
|
|
648
|
-
//
|
|
649
|
-
// 1. a scalar field of exactly that name -> itself
|
|
650
|
-
// 2. the part names this entity, and there is a `name` -> `name`
|
|
651
|
-
// (`/repos/{owner}/{repo}` on entity `repo`, whose response calls the
|
|
652
|
-
// repository `name`)
|
|
653
|
-
// 3. a scalar `<part>_name` / `<part>_login` / `<part>_slug`
|
|
654
|
-
// 4. an OBJECT field of that name -> `<part>.<conventional key>`
|
|
655
|
-
// (`owner` is a user object; the value is `owner.login`)
|
|
656
|
-
//
|
|
657
|
-
// A part none of these resolve is left OUT. Downstream then knows the id
|
|
658
|
-
// cannot be rebuilt for that entity and can say so, which is better than a
|
|
659
|
-
// confidently wrong id on a real record. guide.aon can state it instead.
|
|
660
453
|
function identityFrom(
|
|
661
454
|
ment: ModelEntity,
|
|
662
455
|
parts: string[],
|
|
663
456
|
def: any,
|
|
664
457
|
): Record<string, string> {
|
|
665
|
-
// THE RESPONSE SCHEMA IS THE AUTHORITY, not `ment.fields`.
|
|
666
|
-
//
|
|
667
|
-
// `ment.fields` is merged across load, create, update and list, so a part
|
|
668
|
-
// that exists only in a REQUEST BODY appears there too. Resolving against
|
|
669
|
-
// it recorded such a part in `from` as though a returned record carried it,
|
|
670
|
-
// and a consumer then rebuilt an id from a property the response never
|
|
671
|
-
// sends — worse than leaving the part unresolved, which at least says so.
|
|
672
|
-
//
|
|
673
|
-
// Candidate property maps, in order: the response's own properties, then
|
|
674
|
-
// one level into an envelope. A response that wraps the record
|
|
675
|
-
// (`{ item: {...} }`, `{ data: [ {...} ] }`) states the record's fields one
|
|
676
|
-
// level in, and searching only the wrapper found nothing.
|
|
677
458
|
const candidates = responseCandidates(ment, def)
|
|
678
459
|
const out: Record<string, string> = {}
|
|
679
460
|
|
|
680
461
|
for (const part of parts) {
|
|
681
|
-
// THE WIRE NAME AS WELL AS THE MODEL NAME. `identityParams` reads the
|
|
682
|
-
// RENAMED parameter off the path segments, while a response keeps its own
|
|
683
|
-
// casing — so a `tenantKey` renamed to `tenant_key` was looked up under a
|
|
684
|
-
// name the response does not use, and the mapping was dropped for every
|
|
685
|
-
// camel-cased or depluralized parameter.
|
|
686
462
|
const aliases = partAliases(ment, part)
|
|
687
463
|
|
|
688
464
|
let found: string | null = null
|
|
@@ -710,15 +486,6 @@ function scalarStringField(f: any): boolean {
|
|
|
710
486
|
}
|
|
711
487
|
|
|
712
488
|
|
|
713
|
-
// WHICH PARAMETER IS THE RECORD'S OWN KEY, among several that looked
|
|
714
|
-
// adjacent. The same shape apidef's id handling recognises everywhere else:
|
|
715
|
-
//
|
|
716
|
-
// 1. one named exactly `id`
|
|
717
|
-
// 2. `<entity>_id` — the entity's own id, however the path spells it
|
|
718
|
-
// 3. any `*_id` — an id by name
|
|
719
|
-
// 4. failing all that, the terminal parameter
|
|
720
|
-
//
|
|
721
|
-
// Position is the LAST resort, not the first.
|
|
722
489
|
function singleKeyOf(ment: ModelEntity, parts: string[]): string | undefined {
|
|
723
490
|
if (0 === parts.length) {
|
|
724
491
|
return undefined
|
|
@@ -731,12 +498,6 @@ function singleKeyOf(ment: ModelEntity, parts: string[]): string | undefined {
|
|
|
731
498
|
}
|
|
732
499
|
|
|
733
500
|
|
|
734
|
-
// The composite half of the id descriptor, or `{}` for the ordinary case.
|
|
735
|
-
//
|
|
736
|
-
// Emitted ONLY for a genuinely composite id (two or more addressing
|
|
737
|
-
// parameters). A single-parameter entity already round-trips through one
|
|
738
|
-
// `id` and gains nothing from carrying a one-element `parts`, so its
|
|
739
|
-
// descriptor is left exactly as it was — no existing model output moves.
|
|
740
501
|
function compositeId(
|
|
741
502
|
ment: ModelEntity,
|
|
742
503
|
gent?: any,
|
|
@@ -749,17 +510,6 @@ function compositeId(
|
|
|
749
510
|
// empty `parts`, because aontu resolves an empty list to nothing and the
|
|
750
511
|
// key would arrive absent — indistinguishable from never having been set.
|
|
751
512
|
if (null != gid && false === gid.composite) {
|
|
752
|
-
// DISABLING COMPOSITE MUST NOT DISABLE THE ID. The correction says these
|
|
753
|
-
// adjacent parameters are not a compound key; it does not say the record
|
|
754
|
-
// has no key. Returning a bare `{}` left an entity whose response has no
|
|
755
|
-
// literal `id` with no descriptor at all — the false positive removed and
|
|
756
|
-
// nothing identifying the real key.
|
|
757
|
-
//
|
|
758
|
-
// WHICH of the adjacent parameters is that key is decided by the same
|
|
759
|
-
// id-finding rules apidef uses elsewhere, not by position. Taking the
|
|
760
|
-
// terminal one picked `archive_format` for
|
|
761
|
-
// `/artifacts/{artifact_id}/{archive_format}` — the modifier, precisely
|
|
762
|
-
// the false positive the correction exists to undo.
|
|
763
513
|
return { single: singleKeyOf(ment, identityParams(ment)) } as any
|
|
764
514
|
}
|
|
765
515
|
|
|
@@ -834,19 +584,6 @@ function resolveOpFields(
|
|
|
834
584
|
req: !!fielddef.required,
|
|
835
585
|
op: {},
|
|
836
586
|
}
|
|
837
|
-
// Carry the spec's own words for the field, when it has any.
|
|
838
|
-
//
|
|
839
|
-
// Every generated per-entity table has a Description column and every cell
|
|
840
|
-
// was blank, because nothing ever read the property `description` the spec
|
|
841
|
-
// supplies. Trimmed, and only when it is a non-empty string: a whitespace
|
|
842
|
-
// or non-string value would put a meaningless cell where an empty one is
|
|
843
|
-
// honest.
|
|
844
|
-
// ONE LINE, not the whole description. Every generated Readme drops this
|
|
845
|
-
// straight into a markdown table cell, where a raw newline ends the row
|
|
846
|
-
// and orphans the rest of the table — and specs put bullet lists, fenced
|
|
847
|
-
// examples and multi-paragraph notes in `description`. firstSentence is
|
|
848
|
-
// the same reduction the API summary uses, so `short` means the same
|
|
849
|
-
// thing wherever it appears.
|
|
850
587
|
const fdesc = (fielddef as any).description
|
|
851
588
|
if ('string' === typeof fdesc && '' !== fdesc.trim()) {
|
|
852
589
|
const short = firstSentence(fdesc)
|
|
@@ -855,20 +592,6 @@ function resolveOpFields(
|
|
|
855
592
|
}
|
|
856
593
|
}
|
|
857
594
|
|
|
858
|
-
// SPEC FACTS ABOUT THE FIELD, carried through verbatim.
|
|
859
|
-
//
|
|
860
|
-
// These four are declared by OpenAPI on the property and were being
|
|
861
|
-
// dropped on the floor. `readOnly` is the one that matters most: it is
|
|
862
|
-
// the difference between a field a client MAY send and one it may not,
|
|
863
|
-
// and nothing else in the model says which — so every generator has been
|
|
864
|
-
// putting server-assigned fields into the type a caller fills in.
|
|
865
|
-
//
|
|
866
|
-
// ONLY WHEN THE SPEC SAYS SO, and for the booleans only when TRUE. Each
|
|
867
|
-
// defaults to false in OpenAPI, so an absent key and an explicit `false`
|
|
868
|
-
// carry the same information; emitting the false ones would add a key to
|
|
869
|
-
// every field of every model and say nothing. Same discipline as
|
|
870
|
-
// `short`: absent means "the spec did not say", never "apidef dropped
|
|
871
|
-
// it".
|
|
872
595
|
for (const flag of ['readOnly', 'writeOnly', 'deprecated'] as const) {
|
|
873
596
|
if (true === (fielddef as any)[flag]) {
|
|
874
597
|
mfield[flag] = true
|
|
@@ -918,7 +641,6 @@ function findGraphqlFieldDefs(
|
|
|
918
641
|
|
|
919
642
|
const out: SchemaDef[] = []
|
|
920
643
|
|
|
921
|
-
// Sorted by construction in parse/graphql.ts, so output stays byte-stable.
|
|
922
644
|
for (const fname of Object.keys(gtype.fields)) {
|
|
923
645
|
const f = gtype.fields[fname]
|
|
924
646
|
|
|
@@ -941,17 +663,10 @@ function findGraphqlFieldDefs(
|
|
|
941
663
|
// custom scalars left unconstrained.
|
|
942
664
|
type: 'ENUM' === kind ? 'string' : gqlFieldType(f.type),
|
|
943
665
|
required: f.reqd,
|
|
944
|
-
// GraphQL puts the field's own words on GqlField.desc (see
|
|
945
|
-
// parse/graphql.ts). resolveOpFields reads `description`, the OpenAPI
|
|
946
|
-
// spelling, so name it that here rather than teaching the reader two.
|
|
947
666
|
description: f.desc,
|
|
948
667
|
} as any)
|
|
949
668
|
}
|
|
950
669
|
else if (('OBJECT' === kind || 'INTERFACE' === kind) && !f.list) {
|
|
951
|
-
// To-one relation. The default fragment selects `team { id }`, so the
|
|
952
|
-
// response carries a nested stub object — declare it as such. Naming a
|
|
953
|
-
// flat `team_id` here would advertise a field the wire never returns,
|
|
954
|
-
// since nothing flattens the response.
|
|
955
670
|
const idField = ftype.fields?.id
|
|
956
671
|
if (null != idField) {
|
|
957
672
|
out.push({
|
|
@@ -968,12 +683,6 @@ function findGraphqlFieldDefs(
|
|
|
968
683
|
}
|
|
969
684
|
|
|
970
685
|
|
|
971
|
-
// GraphQL named type -> the type names the field typing understands.
|
|
972
|
-
//
|
|
973
|
-
// Built-ins only: a custom scalar (JSON, JSONObject, Upload, ...) can hold
|
|
974
|
-
// any JSON value, so advertising it as a string would misdescribe the data
|
|
975
|
-
// and make generated validation reject values the schema accepts. Enums are
|
|
976
|
-
// mapped by the caller, which knows they are strings.
|
|
977
686
|
function gqlFieldType(typeName: string): string | undefined {
|
|
978
687
|
return 'Int' === typeName ? 'integer' :
|
|
979
688
|
'Float' === typeName ? 'number' :
|
|
@@ -984,20 +693,24 @@ function gqlFieldType(typeName: string): string | undefined {
|
|
|
984
693
|
|
|
985
694
|
|
|
986
695
|
function findFieldDefs(
|
|
987
|
-
|
|
696
|
+
ment: ModelEntity,
|
|
988
697
|
mop: ModelOp,
|
|
989
698
|
mpoint: ModelPoint,
|
|
990
699
|
def: any
|
|
991
700
|
): SchemaDef[] {
|
|
992
701
|
if ('graphql' === mpoint.kind) {
|
|
993
|
-
return findGraphqlFieldDefs(
|
|
702
|
+
return findGraphqlFieldDefs(ment, mpoint, def)
|
|
994
703
|
}
|
|
995
704
|
|
|
705
|
+
// A verb, rather than an address: see the call site in the transform.
|
|
706
|
+
const isAction = null != (mpoint as any)?.select?.['$action']
|
|
707
|
+
|
|
996
708
|
const fielddefs: SchemaDef[] = []
|
|
709
|
+
|
|
997
710
|
const pathdef = def.paths[mpoint.orig]
|
|
998
711
|
|
|
999
712
|
const method = mpoint.method.toLowerCase()
|
|
1000
|
-
const opdef: any = pathdef[method]
|
|
713
|
+
const opdef: any = (pathdef as any)?.[method]
|
|
1001
714
|
|
|
1002
715
|
if (opdef) {
|
|
1003
716
|
const responses = opdef.responses
|
|
@@ -1009,13 +722,6 @@ function findFieldDefs(
|
|
|
1009
722
|
fieldSets = getx(responses, '200 content "application/json" schema') ??
|
|
1010
723
|
getx(responses, '200 schema')
|
|
1011
724
|
if ('list' == mop.name) {
|
|
1012
|
-
// List responses commonly come in three shapes:
|
|
1013
|
-
// 1. direct array — { type: array, items: { ...item } }
|
|
1014
|
-
// 2. wrapper object — { properties: { items: [Item], page, ... } }
|
|
1015
|
-
// (a single array-of-object property inside an object schema)
|
|
1016
|
-
// 3. legacy "list of created items" under 201
|
|
1017
|
-
// Resolve to the inner item schema when we can identify one
|
|
1018
|
-
// unambiguously; otherwise fall through to the 200 schema as-is.
|
|
1019
725
|
const unwrapped = unwrapArrayWrapper(fieldSets)
|
|
1020
726
|
if (unwrapped) {
|
|
1021
727
|
fieldSets = unwrapped
|
|
@@ -1031,14 +737,6 @@ function findFieldDefs(
|
|
|
1031
737
|
getx(responses, '201 schema')
|
|
1032
738
|
}
|
|
1033
739
|
|
|
1034
|
-
// Single-entity responses get the same treatment the list branch above
|
|
1035
|
-
// already gives collections: a body that is only an envelope around the
|
|
1036
|
-
// entity — `{item: {...}}` — describes the WRAPPER, not the entity, so
|
|
1037
|
-
// its sole property would otherwise be harvested as a field. That is
|
|
1038
|
-
// how an entity `todoitem` ended up with a required `item` field of
|
|
1039
|
-
// type object, which then appeared in the generated create/update data
|
|
1040
|
-
// types. envelopeProp applies the same two rules used to pick the
|
|
1041
|
-
// response transform, so the field list and the transform agree.
|
|
1042
740
|
if ('list' != mop.name) {
|
|
1043
741
|
const envelope = envelopeProp(fieldSets?.properties, mop.name)
|
|
1044
742
|
if (null != envelope) {
|
|
@@ -1047,11 +745,16 @@ function findFieldDefs(
|
|
|
1047
745
|
}
|
|
1048
746
|
}
|
|
1049
747
|
|
|
748
|
+
if (isAction && !namesEntity(fieldSets, ment)) {
|
|
749
|
+
return fielddefs
|
|
750
|
+
}
|
|
751
|
+
|
|
1050
752
|
// A QUERY (RFC 10008) request body is a filter/query schema, not the
|
|
1051
753
|
// entity shape, so it must not contribute entity fields. Fields for a
|
|
1052
754
|
// QUERY op come from its response only. Other methods (POST/PUT/PATCH)
|
|
1053
|
-
// carry the entity in the body, so merge as usual
|
|
1054
|
-
|
|
755
|
+
// carry the entity in the body, so merge as usual -- except for an
|
|
756
|
+
// action, whose body is the verb's arguments and never the record.
|
|
757
|
+
if (requestBody && 'query' !== method && !isAction) {
|
|
1055
758
|
fieldSets = [
|
|
1056
759
|
fieldSets,
|
|
1057
760
|
getx(requestBody, 'content "application/json" schema') ??
|
|
@@ -1149,7 +852,6 @@ function findExampleObject(opdef: any): any {
|
|
|
1149
852
|
example = getx(resdef, 'examples "application/json"')
|
|
1150
853
|
if (null != example && 'object' === typeof example) return unwrapExample(example)
|
|
1151
854
|
|
|
1152
|
-
// Swagger 2.0: schema.example
|
|
1153
855
|
example = getx(resdef, 'schema example')
|
|
1154
856
|
if (null != example && 'object' === typeof example) return unwrapExample(example)
|
|
1155
857
|
|
|
@@ -1166,16 +868,6 @@ function unwrapExample(example: any): any {
|
|
|
1166
868
|
}
|
|
1167
869
|
|
|
1168
870
|
|
|
1169
|
-
// unwrapArrayWrapper inspects a list-response schema and, when it is an
|
|
1170
|
-
// object with a single array-of-object-schema property (e.g.
|
|
1171
|
-
// { boards: [Board] }, { items: [Foo], page, total, ... }), returns the
|
|
1172
|
-
// inner item schema so that field resolution sees the actual entity
|
|
1173
|
-
// properties rather than the wrapper's bookkeeping.
|
|
1174
|
-
//
|
|
1175
|
-
// Returns null if the input is not unambiguously such a wrapper:
|
|
1176
|
-
// - schema is already an array → return null (let caller use it directly)
|
|
1177
|
-
// - no array-of-object-schema property → return null
|
|
1178
|
-
// - more than one array-of-object-schema property → ambiguous, return null
|
|
1179
871
|
function unwrapArrayWrapper(schema: any): any {
|
|
1180
872
|
if (null == schema || 'object' !== typeof schema) return null
|
|
1181
873
|
// Direct list shape — caller can resolve from items directly.
|
|
@@ -1227,27 +919,10 @@ function mergeField(
|
|
|
1227
919
|
}
|
|
1228
920
|
}
|
|
1229
921
|
|
|
1230
|
-
// Field identity is first-writer-wins, but a DESCRIPTION is not part of
|
|
1231
|
-
// identity: the op that first names a field is often not the one that
|
|
1232
|
-
// documents it (a load response referencing a bare component, a create body
|
|
1233
|
-
// referencing the annotated one). Take the first non-empty description in
|
|
1234
|
-
// opFieldPrecedence order and keep it — dropping it left a blank cell in
|
|
1235
|
-
// every generated table while the spec had the words all along.
|
|
1236
922
|
if (null == existingField.short && null != newField.short) {
|
|
1237
923
|
existingField.short = newField.short
|
|
1238
924
|
}
|
|
1239
925
|
|
|
1240
|
-
// The spec facts merge the same way, and for the same reason: one schema
|
|
1241
|
-
// annotates the field and another references it bare, so taking the first
|
|
1242
|
-
// declaration in opFieldPrecedence order is what finds the annotation.
|
|
1243
|
-
//
|
|
1244
|
-
// THE PRECEDENCE ORDER PUTS `load` FIRST, WHICH IS THE SAFE DIRECTION HERE.
|
|
1245
|
-
// A field the response schema marks readOnly and a request body also lists
|
|
1246
|
-
// is a self-contradictory spec — OpenAPI says a client must not send a
|
|
1247
|
-
// readOnly property at all — and this resolves it by believing the
|
|
1248
|
-
// restriction rather than the omission. Marking a writable field readOnly
|
|
1249
|
-
// costs a caller one field; the other way round sends a value the server
|
|
1250
|
-
// rejects.
|
|
1251
926
|
for (const flag of ['readOnly', 'writeOnly', 'deprecated', 'format'] as const) {
|
|
1252
927
|
if (null == existingField[flag] && null != newField[flag]) {
|
|
1253
928
|
(existingField as any)[flag] = newField[flag]
|