@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/guide/heuristic01.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
capture,
|
|
48
48
|
cleanComponentName,
|
|
49
49
|
debugpath,
|
|
50
|
+
debugpathOn,
|
|
50
51
|
depluralize,
|
|
51
52
|
ensureMinEntityName,
|
|
52
53
|
find,
|
|
@@ -112,7 +113,6 @@ const METHOD_CONSIDER_ORDER: Record<string, number> = {
|
|
|
112
113
|
|
|
113
114
|
async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
|
|
114
115
|
|
|
115
|
-
// TODO: Ordu needs better debug output to track task exec
|
|
116
116
|
const analysis = new Ordu({ select: { sort: true } }).add([
|
|
117
117
|
Prepare,
|
|
118
118
|
{
|
|
@@ -145,12 +145,6 @@ async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
|
|
|
145
145
|
|
|
146
146
|
const guide = result.data.guide
|
|
147
147
|
|
|
148
|
-
// Reassign single-segment collection paths (e.g. "/people") onto the entity
|
|
149
|
-
// that owns the per-instance path ("/people/{id}"). Heuristic discovery can
|
|
150
|
-
// split the two when response schemas wrap the resource in a search/pagination
|
|
151
|
-
// component. Running this here — before base-guide.aontu is serialised —
|
|
152
|
-
// means the same merged layout flows into both guide-case (transformers off)
|
|
153
|
-
// and model-case (transformers on), so they agree on the final guide.
|
|
154
148
|
mergeCollectionPaths(guide, ctx.log)
|
|
155
149
|
|
|
156
150
|
const metrics = guide.metrics
|
|
@@ -227,7 +221,6 @@ function Prepare(spec: TaskSpec) {
|
|
|
227
221
|
function MeasurePath(spec: TaskSpec) {
|
|
228
222
|
const guide = spec.data.guide
|
|
229
223
|
const metrics = guide.metrics
|
|
230
|
-
// const pathstr = spec.node.key
|
|
231
224
|
const pathdef = spec.node.val
|
|
232
225
|
|
|
233
226
|
metrics.count.path++
|
|
@@ -250,7 +243,6 @@ function MeasurePath(spec: TaskSpec) {
|
|
|
250
243
|
function MeasureMethod(spec: TaskSpec) {
|
|
251
244
|
const guide = spec.data.guide
|
|
252
245
|
const metrics = guide.metrics
|
|
253
|
-
// const methodstr = spec.node.key
|
|
254
246
|
const methoddef = spec.node.val
|
|
255
247
|
|
|
256
248
|
const pathtags = methoddef.tags
|
|
@@ -316,31 +308,7 @@ function MeasureRef(spec: TaskSpec) {
|
|
|
316
308
|
|
|
317
309
|
function selectAllMethods(_source: any, spec: TaskSpec): MethodDesc[] {
|
|
318
310
|
const ctx = spec.ctx
|
|
319
|
-
// const paths = ctx.def.paths
|
|
320
|
-
|
|
321
|
-
/*
|
|
322
|
-
let caught = capture(ctx.def, {
|
|
323
|
-
paths:
|
|
324
|
-
['`$SELECT`', /.* /,
|
|
325
|
-
['`$SELECT`', /^get|post|put|patch|delete$/i,
|
|
326
|
-
['`$APPEND`', 'methods', {
|
|
327
|
-
path: '`select$=key.paths`',
|
|
328
|
-
method: { '`$UPPER`': '`$KEY`' },
|
|
329
|
-
summary: '`.summary`',
|
|
330
|
-
tags: '`.tags`',
|
|
331
|
-
parameters: '`.parameters`',
|
|
332
|
-
responses: '`.responses`',
|
|
333
|
-
requestBody: '`.requestBody`'
|
|
334
|
-
}]
|
|
335
|
-
]
|
|
336
|
-
]
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
// TODO: capture should return these empty objects
|
|
340
|
-
caught = caught ?? {}
|
|
341
|
-
caught.methods = caught.methods ?? []
|
|
342
311
|
|
|
343
|
-
*/
|
|
344
312
|
|
|
345
313
|
let caught: any = { methods: [] }
|
|
346
314
|
|
|
@@ -424,7 +392,6 @@ function ResolveEntityComponent(spec: TaskSpec) {
|
|
|
424
392
|
})
|
|
425
393
|
.filter(xref => null != xref.cmp)
|
|
426
394
|
|
|
427
|
-
// TODO: identify non - ent schemas
|
|
428
395
|
.filter(xref => !xref.val.includes('Meta'))
|
|
429
396
|
|
|
430
397
|
let cleanxrefs = cmpxrefs
|
|
@@ -607,19 +574,6 @@ function ResolveEntityName(spec: TaskSpec) {
|
|
|
607
574
|
}
|
|
608
575
|
}
|
|
609
576
|
|
|
610
|
-
// A PATH SEGMENT CARRIES NO WORD BOUNDARIES, and the component name does.
|
|
611
|
-
//
|
|
612
|
-
// `/openbanking/payeeverification` is one lowercase run, so `canonize` has
|
|
613
|
-
// nothing to split on and the entity is named `payeeverification` ->
|
|
614
|
-
// `Payeeverification`. The boundary was never missing: the response
|
|
615
|
-
// component for that same operation is
|
|
616
|
-
// `PayeeVerification.PayeeVerificationResult`, which canonizes to
|
|
617
|
-
// `payee_verification_result`.
|
|
618
|
-
//
|
|
619
|
-
// Where the path wins over the component -- which is usually RIGHT, because
|
|
620
|
-
// a widely-reused response schema makes a poor entity name -- the component
|
|
621
|
-
// was being discarded whole, boundaries and all. Borrow just the boundaries
|
|
622
|
-
// back. See resplitFromCmp for why this cannot invent a name.
|
|
623
577
|
entname = resplitFromCmp(entname, ment.cmp as string, why_path)
|
|
624
578
|
|
|
625
579
|
// Keep the pre-truncation name so a truncated-name collision can tell a
|
|
@@ -658,8 +612,11 @@ function ResolveEntityName(spec: TaskSpec) {
|
|
|
658
612
|
work.pathowner[pathStr] = work.pathowner[pathStr] ?? {}
|
|
659
613
|
work.pathowner[pathStr][methodName] = entname
|
|
660
614
|
|
|
661
|
-
|
|
662
|
-
|
|
615
|
+
// Same guard, same reason: the formatting is the cost, not the call.
|
|
616
|
+
if (debugpathOn()) {
|
|
617
|
+
debugpath(pathStr, methodName, 'RESOLVE-ENTITY-NAME',
|
|
618
|
+
formatJSONIC({ entdesc, ment }, { hsepd: 0, $: true, color: true }))
|
|
619
|
+
}
|
|
663
620
|
}
|
|
664
621
|
|
|
665
622
|
|
|
@@ -683,12 +640,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
683
640
|
|
|
684
641
|
const methodName = mdesc.method
|
|
685
642
|
|
|
686
|
-
// Rewrite path parameters that are identifiers to follow the rules:
|
|
687
|
-
// 0. Parameters named [a-z]?id are considered identifiers
|
|
688
|
-
// 1. last identifier is always {id} as this is the primary entity
|
|
689
|
-
// 2. internal identifiers are formatted as {name_id} where name is the parent entity name
|
|
690
|
-
// Example: /api/bar/{id}/zed/{zid}/foo/{fid} ->
|
|
691
|
-
// /api/bar/{bar_id}/zed/{zed_id}/foo/{id}
|
|
692
643
|
|
|
693
644
|
const pathDesc = entdesc.path[pathStr]
|
|
694
645
|
pathDesc.rename = (pathDesc.rename ?? { param: {} })
|
|
@@ -703,12 +654,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
703
654
|
}
|
|
704
655
|
const parts = pathdesc.parts
|
|
705
656
|
|
|
706
|
-
// Implicit snake_case normalization for any path placeholder not already
|
|
707
|
-
// renamed by the id-rename logic. apidef's args transform snake-cases param
|
|
708
|
-
// names (e.g. spec `platformKey` → param.name `platform_key`); without
|
|
709
|
-
// normalizing the placeholder to match, runtime URL substitution by
|
|
710
|
-
// param.name fails to fill `{platformKey}`. Defined as a closure so we can
|
|
711
|
-
// run it after the id-rename loop OR after the multi-param early-return.
|
|
712
657
|
const applySnakeCaseRename = () => {
|
|
713
658
|
for (const part of parts) {
|
|
714
659
|
const m = part.match(/^\{(.+)\}$/)
|
|
@@ -798,7 +743,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
798
743
|
|| parentName === cmpname
|
|
799
744
|
)
|
|
800
745
|
) {
|
|
801
|
-
// let newParamName = 'id'
|
|
802
746
|
updateParamRename(
|
|
803
747
|
ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
|
|
804
748
|
'id', 'action-parent:' + entdesc.name)
|
|
@@ -897,8 +841,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
897
841
|
'id', 'id-not-last')
|
|
898
842
|
|
|
899
843
|
why.push('id-not-last')
|
|
900
|
-
// paramRenames[oldParam] = 'id'
|
|
901
|
-
// paramRenamesWhy[oldParam].push('id-not-last')
|
|
902
844
|
}
|
|
903
845
|
|
|
904
846
|
// Not primary ent.
|
|
@@ -912,8 +854,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
912
854
|
newParamName, 'not-primary')
|
|
913
855
|
why.push('not-primary')
|
|
914
856
|
|
|
915
|
-
// paramRenames[oldParam] = newParamName
|
|
916
|
-
// paramRenamesWhy[oldParam].push('not-primary')
|
|
917
857
|
}
|
|
918
858
|
}
|
|
919
859
|
}
|
|
@@ -926,7 +866,6 @@ function RenameParams(spec: TaskSpec) {
|
|
|
926
866
|
delete paramRenameCapture.why[oldParam]
|
|
927
867
|
}
|
|
928
868
|
|
|
929
|
-
// TODO: these need to done via an API
|
|
930
869
|
debugpath(pathStr, methodName, 'RENAME-PARAM',
|
|
931
870
|
{
|
|
932
871
|
pathStr,
|
|
@@ -968,7 +907,6 @@ function FindActions(spec: TaskSpec) {
|
|
|
968
907
|
const entname = ment.entname
|
|
969
908
|
const entdesc = work.entmap[entname]
|
|
970
909
|
|
|
971
|
-
// const pathdesc = spec.data.work.pathmap[pathStr]
|
|
972
910
|
const pathdesc = entdesc.path[pathStr]
|
|
973
911
|
|
|
974
912
|
const methodName = mdesc.method
|
|
@@ -989,12 +927,6 @@ function FindActions(spec: TaskSpec) {
|
|
|
989
927
|
|
|
990
928
|
const cmp = ment.cmp
|
|
991
929
|
|
|
992
|
-
// A verb that ResolveEntityName assigned to its parent entity
|
|
993
|
-
// (verbOnParent) is an action whatever the parent literal canonizes to:
|
|
994
|
-
// `/app/installations/{installation_id}/access_tokens` belongs to `app`.
|
|
995
|
-
// Recorded directly rather than through updateAction, whose guard against
|
|
996
|
-
// an entity "already encoding" the verb would drop `archive` on
|
|
997
|
-
// `email_archive` and leave the verb as a plain CRUD point.
|
|
998
930
|
if (null != ment.verb_on_parent) {
|
|
999
931
|
pathdesc.action[lastPartCanon] = pathdesc.action[lastPartCanon] ?? {
|
|
1000
932
|
why_action: ['ent', entdesc.name, 'verb-on-parent', lastPart, methodName],
|
|
@@ -1068,15 +1000,7 @@ function ResolveOperation(spec: TaskSpec) {
|
|
|
1068
1000
|
return
|
|
1069
1001
|
}
|
|
1070
1002
|
|
|
1071
|
-
// REVIEW: using POST and PUT in non-restian ways is too wierd to handle consistently
|
|
1072
|
-
// correct using guide customizations
|
|
1073
1003
|
|
|
1074
|
-
// Sometimes POST is used to update, not create. Attempt to identify this.
|
|
1075
|
-
// And sometimes vice versa for PUT
|
|
1076
|
-
// const id_param_offset = ment.pm?.expr?.endsWith('/t/') ? 1 : 0
|
|
1077
|
-
// const has_end_id_param =
|
|
1078
|
-
// entname == canonize(parts[parts.length - 2 - id_param_offset])
|
|
1079
|
-
// && parts[parts.length - 1 - id_param_offset]?.toLowerCase().endsWith('id}')
|
|
1080
1004
|
|
|
1081
1005
|
|
|
1082
1006
|
if ('load' === standard_opname) {
|
|
@@ -1084,30 +1008,12 @@ function ResolveOperation(spec: TaskSpec) {
|
|
|
1084
1008
|
opname = islist ? 'list' : opname
|
|
1085
1009
|
}
|
|
1086
1010
|
|
|
1087
|
-
/*
|
|
1088
|
-
else if (
|
|
1089
|
-
'create' === standard_opname
|
|
1090
|
-
&& has_end_id_param
|
|
1091
|
-
) {
|
|
1092
|
-
opname = 'update'
|
|
1093
|
-
why_op.push('id-present')
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
else if (
|
|
1097
|
-
'update' === standard_opname
|
|
1098
|
-
&& !has_end_id_param
|
|
1099
|
-
) {
|
|
1100
|
-
opname = 'create'
|
|
1101
|
-
why_op.push('no-id-present')
|
|
1102
|
-
}
|
|
1103
|
-
*/
|
|
1104
1011
|
|
|
1105
1012
|
|
|
1106
1013
|
else {
|
|
1107
1014
|
why_op.push('not-load')
|
|
1108
1015
|
}
|
|
1109
1016
|
|
|
1110
|
-
// why.push('ent=' + entdesc.name)
|
|
1111
1017
|
|
|
1112
1018
|
ment.opname = opname
|
|
1113
1019
|
ment.why_opname = why_op
|
|
@@ -1154,7 +1060,6 @@ function ResolveTransform(spec: TaskSpec) {
|
|
|
1154
1060
|
const entname = mdesc.MethodEntity.entname
|
|
1155
1061
|
const entdesc = work.entmap[entname]
|
|
1156
1062
|
|
|
1157
|
-
// const pathdesc = spec.data.work.pathmap[pathStr]
|
|
1158
1063
|
const pathdesc = entdesc.path[pathStr]
|
|
1159
1064
|
|
|
1160
1065
|
const methodName = mdesc.method
|
|
@@ -1174,12 +1079,6 @@ function ResolveTransform(spec: TaskSpec) {
|
|
|
1174
1079
|
debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
|
|
1175
1080
|
|
|
1176
1081
|
if (resprops) {
|
|
1177
|
-
// Only unwrap `body.<entity>` when the entity-named response property is
|
|
1178
|
-
// itself a structured value (object/array/ref/composed schema) that could
|
|
1179
|
-
// actually contain the entity. A scalar property that merely shares the
|
|
1180
|
-
// entity's name (e.g. an entity `advice` whose own fields include a
|
|
1181
|
-
// string field `advice`) is a FIELD of the entity, not a wrapper around
|
|
1182
|
-
// it: the response IS the entity, so it must stay `body` (the default).
|
|
1183
1082
|
if (isEntityWrapperProp(resprops[entdesc.origname])) {
|
|
1184
1083
|
transform.res = '`body.' + entdesc.origname + '`'
|
|
1185
1084
|
}
|
|
@@ -1199,14 +1098,6 @@ function ResolveTransform(spec: TaskSpec) {
|
|
|
1199
1098
|
}
|
|
1200
1099
|
}
|
|
1201
1100
|
|
|
1202
|
-
// The SCHEMA is what closedBodyTransform needs (it reads
|
|
1203
|
-
// additionalProperties); the wrapper-name checks need its PROPERTIES. They
|
|
1204
|
-
// used to share one value and index the schema itself, so
|
|
1205
|
-
// `schema['todoitem']` was always undefined and the entity-name request
|
|
1206
|
-
// envelope was never detected — while the Go port read `.properties` and
|
|
1207
|
-
// did detect it. That divergence was inert only because `req` was never
|
|
1208
|
-
// serialised; now that it is, the two implementations would emit different
|
|
1209
|
-
// request bodies for the same spec.
|
|
1210
1101
|
const reqschema = getRequestBodySchema(mdesc.requestBody)
|
|
1211
1102
|
const reqprops = reqschema?.properties
|
|
1212
1103
|
debugpath(pathStr, methodName, 'TRANSFORM-REQ', keysof(reqprops))
|
|
@@ -1276,12 +1167,6 @@ function BuildEntity(spec: TaskSpec) {
|
|
|
1276
1167
|
path,
|
|
1277
1168
|
}
|
|
1278
1169
|
|
|
1279
|
-
// An entity built ENTIRELY out of access-token exchanges is not a resource
|
|
1280
|
-
// — it is the credential plumbing an SDK's auth layer performs. Emitted
|
|
1281
|
-
// with `active: false` rather than dropped, so it stays visible in
|
|
1282
|
-
// guide.aon and the classification can be reversed there (ADR-002): flip
|
|
1283
|
-
// it to `true` and the entity comes back. The exchange itself is recorded
|
|
1284
|
-
// separately as model facts by transform/top.ts.
|
|
1285
1170
|
if (0 < entdesc.authexchange_ops && entdesc.authexchange_ops === entdesc.total_ops) {
|
|
1286
1171
|
guideEntity.active = false
|
|
1287
1172
|
guideEntity.why_inactive = 'auth-exchange'
|
|
@@ -1361,28 +1246,6 @@ function endsWithCmp(data: any, pm: PathMatch) {
|
|
|
1361
1246
|
}
|
|
1362
1247
|
|
|
1363
1248
|
|
|
1364
|
-
// A write on `.../<parent>/{id}/<verb>` is a VERB ON THE PARENT, not an
|
|
1365
|
-
// entity named after its result shape.
|
|
1366
|
-
//
|
|
1367
|
-
// GitHub's `PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge` answers with
|
|
1368
|
-
// a `pull-request-merge-result` component. Naming the method's entity after
|
|
1369
|
-
// that component (the cmp-primary rule) produced a `pull_request_merge_result`
|
|
1370
|
-
// entity with a single `update` op, while the GET on the same path (no
|
|
1371
|
-
// response schema) stayed an action on `pull`: one route split across two
|
|
1372
|
-
// entities by method, and the verb unreachable from the entity it acts on.
|
|
1373
|
-
//
|
|
1374
|
-
// Five signals, together: the method writes (a GET on such a path is a
|
|
1375
|
-
// sub-resource read and keeps the component rule); the response component
|
|
1376
|
-
// occurs nowhere else in the spec (a one-off result, not a resource shape);
|
|
1377
|
-
// that component is not the literal's own collection shape (a create-only
|
|
1378
|
-
// `POST .../{id}/labels` answering with a `label` is a nested collection,
|
|
1379
|
-
// not a verb); the item selector itself (`.../pulls/{pull_number}`) is a
|
|
1380
|
-
// path of the spec, so the trailing literal cannot be a collection of its
|
|
1381
|
-
// own; and nothing extends the path (`.../private-registries/{secret_name}`
|
|
1382
|
-
// makes `private-registries` a collection, whatever its POST answers with).
|
|
1383
|
-
// The verb then joins the parent entity, where FindActions records it as an
|
|
1384
|
-
// action and select stamps `$action` on its points. Returns the parent's
|
|
1385
|
-
// entity name, or null when the rule does not apply.
|
|
1386
1249
|
function verbOnParent(
|
|
1387
1250
|
data: { def: any, guide: any, work: any },
|
|
1388
1251
|
pm: PathMatch,
|
|
@@ -1398,21 +1261,11 @@ function verbOnParent(
|
|
|
1398
1261
|
return null
|
|
1399
1262
|
}
|
|
1400
1263
|
|
|
1401
|
-
// A PLURAL literal names a nested collection, whatever it answers with:
|
|
1402
|
-
// `asset_keys` under `{environment_id}` creates an asset key, and
|
|
1403
|
-
// `approvals` under `{merge_request_iid}` is a collection of approvals.
|
|
1404
|
-
// A verb is singular — `merge`, `revoke`, `resend_confirmation` — so the
|
|
1405
|
-
// component rule below is never reached for a plural, which is what keeps
|
|
1406
|
-
// a create-only collection an entity of its own.
|
|
1407
1264
|
const lit = snakify(getelem(pm, -1))
|
|
1408
1265
|
if ('' === lit || depluralize(lit) !== lit) {
|
|
1409
1266
|
return null
|
|
1410
1267
|
}
|
|
1411
1268
|
|
|
1412
|
-
// A singular literal still names a collection when its response component
|
|
1413
|
-
// is that collection's member shape (`label` answering with `label`, or
|
|
1414
|
-
// with a parent-prefixed `thing_label`); a verb answers with something
|
|
1415
|
-
// else.
|
|
1416
1269
|
const verb = canonize(getelem(pm, -1))
|
|
1417
1270
|
const cmp = String(ment.cmp ?? '')
|
|
1418
1271
|
if ('' === verb || cmp === verb || cmp.endsWith('_' + verb)) {
|
|
@@ -1477,7 +1330,6 @@ function entityPathMatch_tpe(
|
|
|
1477
1330
|
|
|
1478
1331
|
why.push('path=t/p/')
|
|
1479
1332
|
const origPathName = pm[pathNameIndex]
|
|
1480
|
-
// let entname = fixEntName(origPathName)
|
|
1481
1333
|
let entname = canonize(origPathName)
|
|
1482
1334
|
|
|
1483
1335
|
if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
|
|
@@ -1524,7 +1376,6 @@ function entityPathMatch_te(
|
|
|
1524
1376
|
|
|
1525
1377
|
why.push('path=t/')
|
|
1526
1378
|
const origPathName = pm[pathNameIndex]
|
|
1527
|
-
// let entname = fixEntName(origPathName)
|
|
1528
1379
|
let entname = canonize(origPathName)
|
|
1529
1380
|
|
|
1530
1381
|
if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
|
|
@@ -1548,7 +1399,6 @@ function entityPathMatch_tpp(
|
|
|
1548
1399
|
|
|
1549
1400
|
why.push('path=t/p/p')
|
|
1550
1401
|
const origPathName = pm[pathNameIndex]
|
|
1551
|
-
// let entname = fixEntName(origPathName)
|
|
1552
1402
|
let entname = canonize(origPathName)
|
|
1553
1403
|
|
|
1554
1404
|
if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
|
|
@@ -1615,7 +1465,6 @@ function inferEntityName(
|
|
|
1615
1465
|
}
|
|
1616
1466
|
|
|
1617
1467
|
|
|
1618
|
-
// No entity component was found, but there still might be an entity.
|
|
1619
1468
|
function probableEntityMethod(
|
|
1620
1469
|
data: { def: any },
|
|
1621
1470
|
mdesc: any,
|
|
@@ -1796,14 +1645,6 @@ function isListResponse(
|
|
|
1796
1645
|
let islist = false
|
|
1797
1646
|
let schema
|
|
1798
1647
|
|
|
1799
|
-
// 'p/' is anchored (e.g. t/p/): the path ends at a param, so it is an
|
|
1800
|
-
// item path and the response shape cannot change that.
|
|
1801
|
-
//
|
|
1802
|
-
// A bare trailing 'p' (e.g. t/p/p, a compound key like
|
|
1803
|
-
// /repos/{owner}/{repo}) also ends at a param, but the same shape covers
|
|
1804
|
-
// a sub-collection scoped by a compound key (e.g. /audit-log/{ns}/{repo}).
|
|
1805
|
-
// Those are told apart by the response: a collection returns an array at
|
|
1806
|
-
// the top level, an item does not.
|
|
1807
1648
|
const endParamAnchored = !!(pm && pm.expr.endsWith('p/'))
|
|
1808
1649
|
const endParamBare = !!(pm && !endParamAnchored && pm.expr.endsWith('p'))
|
|
1809
1650
|
|
|
@@ -1823,11 +1664,6 @@ function isListResponse(
|
|
|
1823
1664
|
islist = true
|
|
1824
1665
|
}
|
|
1825
1666
|
|
|
1826
|
-
// The array-prop fallback is deliberately loose, and an item schema
|
|
1827
|
-
// often carries an incidental array property (GitHub's full-repository
|
|
1828
|
-
// has topics: string[]). That is good enough evidence for an ordinary
|
|
1829
|
-
// path, but not for a compound-key path, where it is exactly what
|
|
1830
|
-
// misclassifies the item as a list.
|
|
1831
1667
|
if (!islist && !endParamBare) {
|
|
1832
1668
|
const properties = resolveSchemaProperties(schema)
|
|
1833
1669
|
|
|
@@ -1911,6 +1747,47 @@ function updateParamRename(
|
|
|
1911
1747
|
newParamName: string,
|
|
1912
1748
|
why: string,
|
|
1913
1749
|
) {
|
|
1750
|
+
// A name that cannot be an identifier is not an improvement on the one the
|
|
1751
|
+
// specification gave. See docs/design/derived-names.md
|
|
1752
|
+
if (!/^[A-Za-z_]/.test(newParamName)) {
|
|
1753
|
+
ctx.log.debug({
|
|
1754
|
+
point: 'param-rename-skip',
|
|
1755
|
+
path,
|
|
1756
|
+
param: oldParamName,
|
|
1757
|
+
rejected: newParamName,
|
|
1758
|
+
note: 'derived parameter name is not an identifier, keeping the' +
|
|
1759
|
+
" specification's name"
|
|
1760
|
+
})
|
|
1761
|
+
return
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// The clash is with what the path's OTHER parameters end up called: their
|
|
1765
|
+
// rename if they have one, their canonical name if not.
|
|
1766
|
+
const otherParams = (path.match(/\{([^}]+)\}/g) || [])
|
|
1767
|
+
.map((seg) => seg.slice(1, -1))
|
|
1768
|
+
.filter((name) => name !== oldParamName)
|
|
1769
|
+
|
|
1770
|
+
const takenBy =
|
|
1771
|
+
Object.keys(paramRenameCapture.rename)
|
|
1772
|
+
.find((other) => other !== oldParamName &&
|
|
1773
|
+
paramRenameCapture.rename[other] === newParamName) ??
|
|
1774
|
+
otherParams
|
|
1775
|
+
.find((other) => null == paramRenameCapture.rename[other] &&
|
|
1776
|
+
canonize(other) === newParamName)
|
|
1777
|
+
|
|
1778
|
+
if (null != takenBy) {
|
|
1779
|
+
ctx.log.debug({
|
|
1780
|
+
point: 'param-rename-collision',
|
|
1781
|
+
path,
|
|
1782
|
+
param: oldParamName,
|
|
1783
|
+
rejected: newParamName,
|
|
1784
|
+
takenBy,
|
|
1785
|
+
note: 'another parameter of this path already renames to ' +
|
|
1786
|
+
newParamName + ", keeping the specification's name"
|
|
1787
|
+
})
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1914
1791
|
const existingNewName = paramRenameCapture.rename[oldParamName]
|
|
1915
1792
|
const existingWhy = paramRenameCapture.why[oldParamName]
|
|
1916
1793
|
|
|
@@ -1923,9 +1800,6 @@ function updateParamRename(
|
|
|
1923
1800
|
}
|
|
1924
1801
|
}
|
|
1925
1802
|
else if (newParamName == existingNewName) {
|
|
1926
|
-
// if (!existingWhy.includes(why)) {
|
|
1927
|
-
// existingWhy.push(why)
|
|
1928
|
-
// }
|
|
1929
1803
|
}
|
|
1930
1804
|
else {
|
|
1931
1805
|
ctx.warn({
|
|
@@ -1945,14 +1819,6 @@ function isParam(partStr: string) {
|
|
|
1945
1819
|
}
|
|
1946
1820
|
|
|
1947
1821
|
|
|
1948
|
-
/*
|
|
1949
|
-
function fixEntName(origName: string) {
|
|
1950
|
-
if (null == origName) {
|
|
1951
|
-
return origName
|
|
1952
|
-
}
|
|
1953
|
-
return depluralize(snakify(origName))
|
|
1954
|
-
}
|
|
1955
|
-
*/
|
|
1956
1822
|
|
|
1957
1823
|
|
|
1958
1824
|
function findcmps(
|
|
@@ -1964,7 +1830,6 @@ function findcmps(
|
|
|
1964
1830
|
const cmplist: string[] = []
|
|
1965
1831
|
const cmpset = new Set<string>()
|
|
1966
1832
|
|
|
1967
|
-
// TODO: cache in ctx.work
|
|
1968
1833
|
|
|
1969
1834
|
each(data.def.paths[pathStr])
|
|
1970
1835
|
.map((md: MethodDef) => {
|
package/src/model.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/* Copyright (c) 2024-2025 Voxgig, MIT License */
|
|
2
2
|
|
|
3
|
-
// Consolidated model types for the API model derived from OpenAPI specifications
|
|
4
3
|
|
|
5
4
|
import type { MethodName } from './types'
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
// Operation names available on entities
|
|
9
7
|
type OpName = 'load' | 'list' | 'create' | 'update' | 'remove' | 'patch' | 'head' | 'options'
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
// Argument kinds supported on operation points.
|
|
13
10
|
type ArgKind = 'param' | 'query' | 'header' | 'cookie'
|
|
14
11
|
|
|
15
12
|
|
|
@@ -49,62 +46,36 @@ type Model = NamesCluster & {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
|
|
52
|
-
// Entity relationships information
|
|
53
49
|
type ModelEntityRelations = {
|
|
54
50
|
ancestors: string[][]
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
|
|
58
|
-
// Map of operations available on an entity
|
|
59
54
|
type ModelOpMap = Partial<Record<OpName, ModelOp | undefined>>
|
|
60
55
|
|
|
61
56
|
|
|
62
|
-
// Field-specific operation configuration
|
|
63
57
|
type ModelFieldOp = {
|
|
64
|
-
type: any
|
|
58
|
+
type: any
|
|
65
59
|
req: boolean
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
|
|
69
|
-
// Entity field definition
|
|
70
|
-
//
|
|
71
|
-
// `union` is present only when the field bottoms out in an UNTAGGED union —
|
|
72
|
-
// `oneOf`/`anyOf`, two or more branches, no `discriminator` — so the spec
|
|
73
|
-
// never says which variant a value is and the field can only be modelled as
|
|
74
|
-
// an open type. It records the widest such union found beneath the field, and
|
|
75
|
-
// exists so generators can SAY SO in the documentation rather than silently
|
|
76
|
-
// emitting a permissive type that looks like a modelling failure.
|
|
77
63
|
type ModelField = {
|
|
78
64
|
name: string
|
|
79
|
-
type: any
|
|
65
|
+
type: any
|
|
80
66
|
req: boolean
|
|
81
67
|
op: Partial<Record<OpName, ModelFieldOp>>
|
|
82
68
|
|
|
83
|
-
// One-line human description, straight from the spec's property
|
|
84
|
-
// `description`. Absent when the spec does not describe the property —
|
|
85
|
-
// generators render an empty cell rather than inventing prose.
|
|
86
69
|
short?: string
|
|
87
70
|
|
|
88
|
-
// SPEC FACTS ABOUT THE FIELD ITSELF, carried through verbatim from the
|
|
89
|
-
// OpenAPI property. Facts the spec states, not inferences.
|
|
90
|
-
//
|
|
91
|
-
// `readOnly` is the load-bearing one: it is the difference between a field
|
|
92
|
-
// a client MAY send and one it may not, which nothing else in this record
|
|
93
|
-
// expresses. Without it every generator necessarily puts server-assigned
|
|
94
|
-
// fields into the type a caller fills in.
|
|
95
|
-
//
|
|
96
|
-
// The booleans are present ONLY when the spec declares them true — each
|
|
97
|
-
// defaults to false in OpenAPI, so absent and explicit-false mean the same
|
|
98
|
-
// thing and emitting the false ones would change every model for no
|
|
99
|
-
// information. `format` is present only for a non-empty string.
|
|
100
71
|
readOnly?: boolean
|
|
101
72
|
writeOnly?: boolean
|
|
102
73
|
deprecated?: boolean
|
|
103
74
|
format?: string
|
|
104
75
|
union?: {
|
|
105
|
-
count: number
|
|
106
|
-
branches: number
|
|
107
|
-
depth: number
|
|
76
|
+
count: number
|
|
77
|
+
branches: number
|
|
78
|
+
depth: number
|
|
108
79
|
}
|
|
109
80
|
}
|
|
110
81
|
|
|
@@ -117,7 +88,7 @@ type ModelField = {
|
|
|
117
88
|
type ModelArg = {
|
|
118
89
|
name: string
|
|
119
90
|
orig: string
|
|
120
|
-
type: any
|
|
91
|
+
type: any
|
|
121
92
|
kind: ArgKind
|
|
122
93
|
reqd: boolean
|
|
123
94
|
example?: any
|
|
@@ -131,8 +102,6 @@ type ModelArg = {
|
|
|
131
102
|
type PointKind = 'http' | 'graphql'
|
|
132
103
|
|
|
133
104
|
|
|
134
|
-
// Pagination descriptor for a GraphQL list op. `nodes`/`cursor`/`more` are
|
|
135
|
-
// dotted paths relative to the unwrapped connection object.
|
|
136
105
|
type ModelGraphqlPage = {
|
|
137
106
|
style: string
|
|
138
107
|
nodes: string
|
|
@@ -141,9 +110,6 @@ type ModelGraphqlPage = {
|
|
|
141
110
|
}
|
|
142
111
|
|
|
143
112
|
|
|
144
|
-
// One GraphQL variable binding: `name` is the variable as it appears in the
|
|
145
|
-
// operation document, `from` the op argument it is read from, `gqltype` the
|
|
146
|
-
// declared GraphQL type (e.g. 'String!').
|
|
147
113
|
type ModelGraphqlVar = {
|
|
148
114
|
name: string
|
|
149
115
|
from: string
|
|
@@ -151,9 +117,6 @@ type ModelGraphqlVar = {
|
|
|
151
117
|
}
|
|
152
118
|
|
|
153
119
|
|
|
154
|
-
// GraphQL wire data for a point. `doc` is the complete operation document,
|
|
155
|
-
// rendered single-line with sorted selection fields so output stays
|
|
156
|
-
// byte-stable and schema drift shows up in model diffs.
|
|
157
120
|
type ModelGraphql = {
|
|
158
121
|
optype: 'query' | 'mutation'
|
|
159
122
|
field: string
|
|
@@ -163,13 +126,6 @@ type ModelGraphql = {
|
|
|
163
126
|
}
|
|
164
127
|
|
|
165
128
|
|
|
166
|
-
// One concrete endpoint that can satisfy an operation. An entity op
|
|
167
|
-
// (load/list/create/...) carries an array of these — apidef chooses
|
|
168
|
-
// between them at runtime via `select.exist` matching against reqmatch /
|
|
169
|
-
// reqdata. (Originally named `ModelTarget`; renamed for consistency with
|
|
170
|
-
// the field name `points` and the runtime utility `MakePoint`.)
|
|
171
|
-
// One resolved path segment (ADR-003). Exactly one of `lit` / `var` is set;
|
|
172
|
-
// `var` names an entry of the point's `args.params`.
|
|
173
129
|
type ModelPathSegment = {
|
|
174
130
|
lit?: string
|
|
175
131
|
var?: string
|
|
@@ -206,24 +162,12 @@ type ModelPoint = {
|
|
|
206
162
|
}
|
|
207
163
|
|
|
208
164
|
|
|
209
|
-
// Operation definition
|
|
210
165
|
type ModelOp = {
|
|
211
166
|
name: OpName
|
|
212
167
|
points: ModelPoint[]
|
|
213
168
|
}
|
|
214
169
|
|
|
215
170
|
|
|
216
|
-
// Entity definition - core model entity with operations and fields.
|
|
217
|
-
// `id` is present only when the OpenAPI response/request schema declares
|
|
218
|
-
// (or examples imply) an `id` field on the entity. Public APIs that return
|
|
219
|
-
// payloads without an id (e.g. read-only feeds) leave it undefined.
|
|
220
|
-
//
|
|
221
|
-
// `Name`, `NAME` etc. are stamped on by jostraca's `names()` helper after
|
|
222
|
-
// apidef hands the model to the generator. They're typed as optional here
|
|
223
|
-
// so apidef's transform code can construct entities without them; template
|
|
224
|
-
// code should reach for them through `nom(entity, 'Name')` rather than
|
|
225
|
-
// direct property access, which both works pre-`names()` and lets us
|
|
226
|
-
// remove the optional later.
|
|
227
171
|
type ModelEntity = {
|
|
228
172
|
name: string
|
|
229
173
|
Name?: string
|
|
@@ -233,28 +177,8 @@ type ModelEntity = {
|
|
|
233
177
|
id?: {
|
|
234
178
|
name: string
|
|
235
179
|
field: string
|
|
236
|
-
// COMPOSITE IDENTITY. Present only when the API addresses one record by
|
|
237
|
-
// MORE THAN ONE path parameter, so no single parameter is the id.
|
|
238
|
-
// github's repo is the case: GET /repos/{owner}/{repo} needs both, and
|
|
239
|
-
// neither alone names a repository.
|
|
240
|
-
//
|
|
241
|
-
// `parts` are those parameters in path order; `sep` joins them into the
|
|
242
|
-
// one `id` an SDK entity carries. Absent means the ordinary single-key
|
|
243
|
-
// entity, so downstream can branch on presence alone.
|
|
244
180
|
parts?: string[]
|
|
245
181
|
sep?: string
|
|
246
|
-
// WHERE EACH PART'S VALUE LIVES IN A RESPONSE, as a dotted path.
|
|
247
|
-
//
|
|
248
|
-
// The parts are PATH PARAMETER names and a response names its fields
|
|
249
|
-
// whatever it likes: github addresses a repo by `{owner}/{repo}` and
|
|
250
|
-
// returns the owner as an OBJECT, so the value is at `owner.login`, and
|
|
251
|
-
// the repository under `name`. Without this a consumer can address a
|
|
252
|
-
// record it was given the id of, but cannot put an id on a record the
|
|
253
|
-
// API returned.
|
|
254
|
-
//
|
|
255
|
-
// A part no rule resolves is left OUT, so an incomplete map says the id
|
|
256
|
-
// cannot be rebuilt for that entity — better than a confidently wrong id
|
|
257
|
-
// on a real record. guide.aon can state it instead.
|
|
258
182
|
from?: Record<string, string>
|
|
259
183
|
}
|
|
260
184
|
relations: ModelEntityRelations
|
|
@@ -265,7 +189,6 @@ type ModelEntityFlow = {
|
|
|
265
189
|
name: string,
|
|
266
190
|
entity: string
|
|
267
191
|
kind: string
|
|
268
|
-
// args: Record<string, string>
|
|
269
192
|
step: ModelEntityFlowStep[]
|
|
270
193
|
active?: boolean
|
|
271
194
|
}
|
|
@@ -291,8 +214,6 @@ type ModelEntityFlowStepInput = {
|
|
|
291
214
|
}
|
|
292
215
|
|
|
293
216
|
|
|
294
|
-
// Validators and specs are user-supplied callables identified by the
|
|
295
|
-
// `apply` discriminator; `def` is the validator-specific options bag.
|
|
296
217
|
type ModelEntityFlowStepValidator = {
|
|
297
218
|
apply: string
|
|
298
219
|
def: Record<string, any>
|