@voxgig/apidef 5.11.0 → 6.0.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/transform/entity.js
CHANGED
|
@@ -9,6 +9,16 @@ const entityTransform = async function (ctx) {
|
|
|
9
9
|
const { apimodel, guide } = ctx;
|
|
10
10
|
const kit = apimodel.main[types_1.KIT];
|
|
11
11
|
let msg = '';
|
|
12
|
+
// Pre-pass: merge collection paths into the entity that owns the
|
|
13
|
+
// per-instance paths. Heuristic01 sometimes assigns "/people" to a
|
|
14
|
+
// separate "*_search" entity (because the response wraps Person in
|
|
15
|
+
// a search/pagination component) while "/people/{id}" and
|
|
16
|
+
// "/people/{id}/anime" land on "person". Result: the person entity has
|
|
17
|
+
// no primary list endpoint, so direct-load tests can't bootstrap an
|
|
18
|
+
// ID. Move "/people" onto person here; this also clears the way for
|
|
19
|
+
// sensible flow generation (one entity, one collection, multiple
|
|
20
|
+
// sub-resources).
|
|
21
|
+
mergeCollectionPaths(guide, ctx.log);
|
|
12
22
|
(0, jostraca_1.each)(guide.entity, (guideEntity, entname) => {
|
|
13
23
|
ctx.log.debug({ point: 'guide-entity', note: entname });
|
|
14
24
|
const paths$ = resolvePathList(guideEntity, ctx.def);
|
|
@@ -25,6 +35,76 @@ const entityTransform = async function (ctx) {
|
|
|
25
35
|
return { ok: true, msg };
|
|
26
36
|
};
|
|
27
37
|
exports.entityTransform = entityTransform;
|
|
38
|
+
// Move "/X" paths onto the entity that owns "/X/{id}" or "/X/{id}/sub".
|
|
39
|
+
// Only acts when the path "/X" sits on a different entity than the
|
|
40
|
+
// per-instance paths — leaves correctly-classified APIs alone.
|
|
41
|
+
function mergeCollectionPaths(guide, log) {
|
|
42
|
+
const entities = guide.entity;
|
|
43
|
+
// First pass: build collectionRoot -> owner-entity-name map.
|
|
44
|
+
// owner is the entity whose name contains "/X/{...}" paths; we prefer
|
|
45
|
+
// the owner whose direct-load path is "/X/{id}" (no further segments)
|
|
46
|
+
// so that nested-resource entities don't claim the root.
|
|
47
|
+
const rootOwners = {};
|
|
48
|
+
for (const [ename, entity] of Object.entries(entities)) {
|
|
49
|
+
for (const pathStr of Object.keys(entity.path ?? {})) {
|
|
50
|
+
// Match /A/{...} or /A/{...}/...
|
|
51
|
+
const m = pathStr.match(/^\/([^\/{}]+)\/\{[^}]+\}(\/.*)?$/);
|
|
52
|
+
if (!m)
|
|
53
|
+
continue;
|
|
54
|
+
const root = m[1];
|
|
55
|
+
const trailing = m[2] ?? '';
|
|
56
|
+
// Depth = number of segments after the {id} placeholder. Lower
|
|
57
|
+
// depth wins (e.g. "/people/{id}" beats "/people/{id}/anime").
|
|
58
|
+
const depth = trailing === '' ? 0 : trailing.split('/').filter(Boolean).length;
|
|
59
|
+
const cur = rootOwners[root];
|
|
60
|
+
if (!cur || depth < cur.depth) {
|
|
61
|
+
rootOwners[root] = { ename, depth };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Second pass: for each entity with a "/X" path, if X has an owner
|
|
66
|
+
// elsewhere, move the path there.
|
|
67
|
+
for (const [ename, entity] of Object.entries(entities)) {
|
|
68
|
+
if (entity.path == null)
|
|
69
|
+
continue;
|
|
70
|
+
const pathsToMove = [];
|
|
71
|
+
for (const pathStr of Object.keys(entity.path)) {
|
|
72
|
+
// Match exactly /X (one literal segment, no params).
|
|
73
|
+
const m = pathStr.match(/^\/([^\/{}]+)$/);
|
|
74
|
+
if (!m)
|
|
75
|
+
continue;
|
|
76
|
+
const root = m[1];
|
|
77
|
+
const owner = rootOwners[root];
|
|
78
|
+
if (owner && owner.ename !== ename) {
|
|
79
|
+
pathsToMove.push(pathStr);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const pathStr of pathsToMove) {
|
|
83
|
+
const owner = rootOwners[pathStr.slice(1)];
|
|
84
|
+
const targetEntity = entities[owner.ename];
|
|
85
|
+
if (targetEntity == null)
|
|
86
|
+
continue;
|
|
87
|
+
targetEntity.path = targetEntity.path ?? {};
|
|
88
|
+
// If the target already has this path (unlikely), leave it alone.
|
|
89
|
+
if (targetEntity.path[pathStr] == null) {
|
|
90
|
+
targetEntity.path[pathStr] = entity.path[pathStr];
|
|
91
|
+
}
|
|
92
|
+
delete entity.path[pathStr];
|
|
93
|
+
log?.debug?.({
|
|
94
|
+
point: 'merge-collection-path',
|
|
95
|
+
path: pathStr,
|
|
96
|
+
from: ename,
|
|
97
|
+
to: owner.ename,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Drop entities that are now empty after the merge.
|
|
102
|
+
for (const ename of Object.keys(entities)) {
|
|
103
|
+
if (entities[ename].path == null || Object.keys(entities[ename].path).length === 0) {
|
|
104
|
+
delete entities[ename];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
28
108
|
function resolvePathList(guideEntity, def) {
|
|
29
109
|
const paths$ = [];
|
|
30
110
|
(0, jostraca_1.each)(guideEntity.path, (guidePath, orig) => {
|
|
@@ -49,9 +129,17 @@ function resolvePathList(guideEntity, def) {
|
|
|
49
129
|
return paths$;
|
|
50
130
|
}
|
|
51
131
|
function buildRelations(guideEntity, paths$) {
|
|
132
|
+
// An ancestor is a literal collection segment (e.g. "rems") followed by
|
|
133
|
+
// a path-param placeholder that names an instance ID. We only collect
|
|
134
|
+
// the literal parts — placeholder parts like "{año}" must be excluded
|
|
135
|
+
// even when they're themselves followed by another placeholder, otherwise
|
|
136
|
+
// downstream code treats `{año}` as an ancestor name and emits broken
|
|
137
|
+
// idmap entries / match keys.
|
|
52
138
|
let ancestors = paths$
|
|
53
139
|
.map(pli => pli.parts
|
|
54
|
-
.map((p, i) => (
|
|
140
|
+
.map((p, i) => ('{' !== p[0] &&
|
|
141
|
+
pli.parts[i + 1]?.[0] === '{' &&
|
|
142
|
+
pli.parts[i + 1] !== '{id}') ? p : null)
|
|
55
143
|
.filter(p => null != p))
|
|
56
144
|
.filter(n => 0 < n.length)
|
|
57
145
|
.sort((a, b) => a.length - b.length);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../../src/transform/entity.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../../src/transform/entity.ts"],"names":[],"mappings":"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"}
|
package/dist/transform/top.js
CHANGED
|
@@ -24,6 +24,14 @@ const topTransform = async function (ctx) {
|
|
|
24
24
|
url: (def.schemes?.[0] ?? 'https') + '://' + (0, struct_1.join)([def.host, def.basePath], '/', true)
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
+
// A usable SDK requires a base URL. OpenAPI 3 puts it in `servers[].url`;
|
|
28
|
+
// Swagger 2 derives it from `host` + `basePath`. If neither yields a
|
|
29
|
+
// non-empty url, the generated SDK has no way to issue requests, so fail
|
|
30
|
+
// the apidef model build rather than emit broken code.
|
|
31
|
+
const firstServerUrl = kit.info.servers?.[0]?.url;
|
|
32
|
+
if (null == firstServerUrl || '' === String(firstServerUrl).trim()) {
|
|
33
|
+
throw new Error('apidef: no server URL found in API definition (servers[0].url is required).');
|
|
34
|
+
}
|
|
27
35
|
return { ok: true, msg: 'top' };
|
|
28
36
|
};
|
|
29
37
|
exports.topTransform = topTransform;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"top.js","sourceRoot":"","sources":["../../src/transform/top.ts"],"names":[],"mappings":";;;AACA,2CAAqC;AAErC,oCAA8B;AAmB9B,6BAA6B;AAC7B,yCAAyC;AACzC,8BAA8B;AAC9B,gCAAgC;AAGhC,uBAAuB;AACvB,kBAAkB;AAClB,oCAAoC;AAEpC,uBAAuB;AACvB,iCAAiC;AACjC,IAAI;AAGJ,MAAM,YAAY,GAAG,KAAK,WACxB,GAAQ;IAER,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;IAC7B,MAAM,GAAG,GAAa,QAAQ,CAAC,IAAI,CAAC,WAAG,CAAC,CAAA;IAExC,GAAG,CAAC,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAC/C,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,oBAAoB,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAE1D,cAAc;IACd,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YACpB,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,KAAK,GAAG,IAAA,aAAI,EAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC;SACvF,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACjC,CAAC,CAAA;AA4BC,oCAAY;AAzBd,0EAA0E;AAC1E,wEAAwE;AACxE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,sDAAsD;AACtD,SAAS,oBAAoB,CAAC,IAAS;IACrC,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IAC9D,IAAI,QAAQ,KAAK,OAAO,IAAI,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAwB,EAAE,CAAA;QACnC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,QAAQ,KAAK,OAAO,IAAI,IAAI,SAAS,KAAK,OAAO,IAAI,EAAE,CAAC;QAC1D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAQD,gBAAgB;AAChB,iBAAiB;AACjB,IAAI"}
|
|
1
|
+
{"version":3,"file":"top.js","sourceRoot":"","sources":["../../src/transform/top.ts"],"names":[],"mappings":";;;AACA,2CAAqC;AAErC,oCAA8B;AAmB9B,6BAA6B;AAC7B,yCAAyC;AACzC,8BAA8B;AAC9B,gCAAgC;AAGhC,uBAAuB;AACvB,kBAAkB;AAClB,oCAAoC;AAEpC,uBAAuB;AACvB,iCAAiC;AACjC,IAAI;AAGJ,MAAM,YAAY,GAAG,KAAK,WACxB,GAAQ;IAER,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;IAC7B,MAAM,GAAG,GAAa,QAAQ,CAAC,IAAI,CAAC,WAAG,CAAC,CAAA;IAExC,GAAG,CAAC,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAC/C,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,oBAAoB,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAE1D,cAAc;IACd,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YACpB,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,KAAK,GAAG,IAAA,aAAI,EAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC;SACvF,CAAC,CAAA;IACJ,CAAC;IAED,0EAA0E;IAC1E,qEAAqE;IACrE,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,cAAc,GAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAA;IACtD,IAAI,IAAI,IAAI,cAAc,IAAI,EAAE,KAAK,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,6EAA6E,CAC9E,CAAA;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACjC,CAAC,CAAA;AA4BC,oCAAY;AAzBd,0EAA0E;AAC1E,wEAAwE;AACxE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,sDAAsD;AACtD,SAAS,oBAAoB,CAAC,IAAS;IACrC,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAC7B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IAC9D,IAAI,QAAQ,KAAK,OAAO,IAAI,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAwB,EAAE,CAAA;QACnC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,IAAI,QAAQ,KAAK,OAAO,IAAI,IAAI,SAAS,KAAK,OAAO,IAAI,EAAE,CAAC;QAC1D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAQD,gBAAgB;AAChB,iBAAiB;AACjB,IAAI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voxgig/apidef",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"main": "dist/apidef.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"types": "dist/apidef.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "25.6.
|
|
43
|
+
"@types/node": "25.6.2",
|
|
44
44
|
"typescript": "6.0.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
package/src/transform/entity.ts
CHANGED
|
@@ -31,6 +31,17 @@ const entityTransform: Transform = async function(
|
|
|
31
31
|
|
|
32
32
|
let msg = ''
|
|
33
33
|
|
|
34
|
+
// Pre-pass: merge collection paths into the entity that owns the
|
|
35
|
+
// per-instance paths. Heuristic01 sometimes assigns "/people" to a
|
|
36
|
+
// separate "*_search" entity (because the response wraps Person in
|
|
37
|
+
// a search/pagination component) while "/people/{id}" and
|
|
38
|
+
// "/people/{id}/anime" land on "person". Result: the person entity has
|
|
39
|
+
// no primary list endpoint, so direct-load tests can't bootstrap an
|
|
40
|
+
// ID. Move "/people" onto person here; this also clears the way for
|
|
41
|
+
// sensible flow generation (one entity, one collection, multiple
|
|
42
|
+
// sub-resources).
|
|
43
|
+
mergeCollectionPaths(guide, ctx.log)
|
|
44
|
+
|
|
34
45
|
each(guide.entity, (guideEntity: GuideEntity, entname: string) => {
|
|
35
46
|
ctx.log.debug({ point: 'guide-entity', note: entname })
|
|
36
47
|
|
|
@@ -53,6 +64,81 @@ const entityTransform: Transform = async function(
|
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
|
|
67
|
+
// Move "/X" paths onto the entity that owns "/X/{id}" or "/X/{id}/sub".
|
|
68
|
+
// Only acts when the path "/X" sits on a different entity than the
|
|
69
|
+
// per-instance paths — leaves correctly-classified APIs alone.
|
|
70
|
+
function mergeCollectionPaths(guide: any, log?: any) {
|
|
71
|
+
const entities = guide.entity as Record<string, any>
|
|
72
|
+
|
|
73
|
+
// First pass: build collectionRoot -> owner-entity-name map.
|
|
74
|
+
// owner is the entity whose name contains "/X/{...}" paths; we prefer
|
|
75
|
+
// the owner whose direct-load path is "/X/{id}" (no further segments)
|
|
76
|
+
// so that nested-resource entities don't claim the root.
|
|
77
|
+
const rootOwners: Record<string, { ename: string, depth: number }> = {}
|
|
78
|
+
|
|
79
|
+
for (const [ename, entity] of Object.entries(entities)) {
|
|
80
|
+
for (const pathStr of Object.keys(entity.path ?? {})) {
|
|
81
|
+
// Match /A/{...} or /A/{...}/...
|
|
82
|
+
const m = pathStr.match(/^\/([^\/{}]+)\/\{[^}]+\}(\/.*)?$/)
|
|
83
|
+
if (!m) continue
|
|
84
|
+
const root = m[1]
|
|
85
|
+
const trailing = m[2] ?? ''
|
|
86
|
+
// Depth = number of segments after the {id} placeholder. Lower
|
|
87
|
+
// depth wins (e.g. "/people/{id}" beats "/people/{id}/anime").
|
|
88
|
+
const depth = trailing === '' ? 0 : trailing.split('/').filter(Boolean).length
|
|
89
|
+
|
|
90
|
+
const cur = rootOwners[root]
|
|
91
|
+
if (!cur || depth < cur.depth) {
|
|
92
|
+
rootOwners[root] = { ename, depth }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Second pass: for each entity with a "/X" path, if X has an owner
|
|
98
|
+
// elsewhere, move the path there.
|
|
99
|
+
for (const [ename, entity] of Object.entries(entities)) {
|
|
100
|
+
if (entity.path == null) continue
|
|
101
|
+
const pathsToMove: string[] = []
|
|
102
|
+
|
|
103
|
+
for (const pathStr of Object.keys(entity.path)) {
|
|
104
|
+
// Match exactly /X (one literal segment, no params).
|
|
105
|
+
const m = pathStr.match(/^\/([^\/{}]+)$/)
|
|
106
|
+
if (!m) continue
|
|
107
|
+
const root = m[1]
|
|
108
|
+
const owner = rootOwners[root]
|
|
109
|
+
if (owner && owner.ename !== ename) {
|
|
110
|
+
pathsToMove.push(pathStr)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const pathStr of pathsToMove) {
|
|
115
|
+
const owner = rootOwners[pathStr.slice(1)]
|
|
116
|
+
const targetEntity = entities[owner.ename]
|
|
117
|
+
if (targetEntity == null) continue
|
|
118
|
+
targetEntity.path = targetEntity.path ?? {}
|
|
119
|
+
// If the target already has this path (unlikely), leave it alone.
|
|
120
|
+
if (targetEntity.path[pathStr] == null) {
|
|
121
|
+
targetEntity.path[pathStr] = entity.path[pathStr]
|
|
122
|
+
}
|
|
123
|
+
delete entity.path[pathStr]
|
|
124
|
+
log?.debug?.({
|
|
125
|
+
point: 'merge-collection-path',
|
|
126
|
+
path: pathStr,
|
|
127
|
+
from: ename,
|
|
128
|
+
to: owner.ename,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Drop entities that are now empty after the merge.
|
|
134
|
+
for (const ename of Object.keys(entities)) {
|
|
135
|
+
if (entities[ename].path == null || Object.keys(entities[ename].path).length === 0) {
|
|
136
|
+
delete entities[ename]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
56
142
|
|
|
57
143
|
function resolvePathList(guideEntity: GuideEntity, def: { paths: Record<string, any> }) {
|
|
58
144
|
const paths$: PathDesc[] = []
|
|
@@ -86,10 +172,18 @@ function resolvePathList(guideEntity: GuideEntity, def: { paths: Record<string,
|
|
|
86
172
|
|
|
87
173
|
|
|
88
174
|
function buildRelations(guideEntity: any, paths$: PathDesc[]) {
|
|
175
|
+
// An ancestor is a literal collection segment (e.g. "rems") followed by
|
|
176
|
+
// a path-param placeholder that names an instance ID. We only collect
|
|
177
|
+
// the literal parts — placeholder parts like "{año}" must be excluded
|
|
178
|
+
// even when they're themselves followed by another placeholder, otherwise
|
|
179
|
+
// downstream code treats `{año}` as an ancestor name and emits broken
|
|
180
|
+
// idmap entries / match keys.
|
|
89
181
|
let ancestors: any[] = paths$
|
|
90
182
|
.map(pli => pli.parts
|
|
91
183
|
.map((p, i) =>
|
|
92
|
-
(
|
|
184
|
+
('{' !== p[0] &&
|
|
185
|
+
pli.parts[i + 1]?.[0] === '{' &&
|
|
186
|
+
pli.parts[i + 1] !== '{id}') ? p : null)
|
|
93
187
|
.filter(p => null != p))
|
|
94
188
|
.filter(n => 0 < n.length)
|
|
95
189
|
.sort((a, b) => a.length - b.length)
|
package/src/transform/top.ts
CHANGED
|
@@ -51,6 +51,17 @@ const topTransform = async function(
|
|
|
51
51
|
})
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// A usable SDK requires a base URL. OpenAPI 3 puts it in `servers[].url`;
|
|
55
|
+
// Swagger 2 derives it from `host` + `basePath`. If neither yields a
|
|
56
|
+
// non-empty url, the generated SDK has no way to issue requests, so fail
|
|
57
|
+
// the apidef model build rather than emit broken code.
|
|
58
|
+
const firstServerUrl: any = kit.info.servers?.[0]?.url
|
|
59
|
+
if (null == firstServerUrl || '' === String(firstServerUrl).trim()) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'apidef: no server URL found in API definition (servers[0].url is required).'
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
return { ok: true, msg: 'top' }
|
|
55
66
|
}
|
|
56
67
|
|