@voxgig/apidef 2.4.0 → 3.0.2
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 +5 -1
- package/dist/apidef.js +197 -112
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/entity.d.ts +3 -0
- package/dist/builder/entity/{apiEntity.js → entity.js} +12 -9
- package/dist/builder/entity/entity.js.map +1 -0
- package/dist/builder/entity/info.d.ts +3 -0
- package/dist/builder/entity/info.js +22 -0
- package/dist/builder/entity/info.js.map +1 -0
- package/dist/builder/entity.js +7 -21
- package/dist/builder/entity.js.map +1 -1
- package/dist/builder/flow/flowHeuristic01.js +21 -11
- package/dist/builder/flow/flowHeuristic01.js.map +1 -1
- package/dist/builder/flow.d.ts +2 -1
- package/dist/builder/flow.js +29 -4
- package/dist/builder/flow.js.map +1 -1
- package/dist/def.d.ts +62 -0
- package/dist/def.js +4 -0
- package/dist/def.js.map +1 -0
- package/dist/desc.d.ts +89 -0
- package/dist/desc.js +4 -0
- package/dist/desc.js.map +1 -0
- package/dist/guide/guide.d.ts +2 -1
- package/dist/guide/guide.js +161 -30
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.d.ts +2 -1
- package/dist/guide/heuristic01.js +1120 -234
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/model.d.ts +55 -0
- package/dist/model.js +4 -0
- package/dist/model.js.map +1 -0
- package/dist/parse.d.ts +1 -2
- package/dist/parse.js +8 -47
- package/dist/parse.js.map +1 -1
- package/dist/transform/args.d.ts +3 -0
- package/dist/transform/args.js +58 -0
- package/dist/transform/args.js.map +1 -0
- package/dist/transform/clean.js +27 -3
- package/dist/transform/clean.js.map +1 -1
- package/dist/transform/entity.d.ts +11 -3
- package/dist/transform/entity.js +57 -41
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.d.ts +3 -3
- package/dist/transform/field.js +90 -65
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.d.ts +1 -1
- package/dist/transform/operation.js +94 -296
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.d.ts +3 -0
- package/dist/transform/select.js +44 -0
- package/dist/transform/select.js.map +1 -0
- package/dist/transform/top.d.ts +9 -0
- package/dist/transform/top.js +11 -2
- package/dist/transform/top.js.map +1 -1
- package/dist/transform.js +4 -0
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +112 -19
- package/dist/types.js +4 -2
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +30 -2
- package/dist/utility.js +381 -6
- package/dist/utility.js.map +1 -1
- package/model/apidef.jsonic +75 -1
- package/model/guide.jsonic +14 -44
- package/package.json +19 -14
- package/src/apidef.ts +264 -121
- package/src/builder/entity/{apiEntity.ts → entity.ts} +18 -11
- package/src/builder/entity/info.ts +53 -0
- package/src/builder/entity.ts +9 -35
- package/src/builder/flow/flowHeuristic01.ts +46 -12
- package/src/builder/flow.ts +39 -5
- package/src/def.ts +91 -0
- package/src/desc.ts +143 -0
- package/src/guide/guide.ts +207 -134
- package/src/guide/heuristic01.ts +1651 -272
- package/src/model.ts +98 -0
- package/src/parse.ts +5 -61
- package/src/schematron.ts.off +317 -0
- package/src/transform/args.ts +102 -0
- package/src/transform/clean.ts +43 -8
- package/src/transform/entity.ts +100 -51
- package/src/transform/field.ts +150 -71
- package/src/transform/operation.ts +118 -414
- package/src/transform/select.ts +90 -0
- package/src/transform/top.ts +76 -3
- package/src/transform.ts +4 -0
- package/src/types.ts +185 -5
- package/src/utility.ts +481 -9
- package/dist/builder/entity/apiEntity.d.ts +0 -3
- package/dist/builder/entity/apiEntity.js.map +0 -1
- package/dist/builder/entity/def.d.ts +0 -3
- package/dist/builder/entity/def.js +0 -19
- package/dist/builder/entity/def.js.map +0 -1
- package/src/builder/entity/def.ts +0 -44
- package/src/guide.ts.off +0 -136
package/src/model.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* Copyright (c) 2024-2025 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
// Consolidated model types for the API model derived from OpenAPI specifications
|
|
4
|
+
|
|
5
|
+
import type { MethodName } from './types'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Operation names available on entities
|
|
9
|
+
type OpName = 'load' | 'list' | 'create' | 'update' | 'delete' | 'patch' | 'head' | 'options'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Entity relationships information
|
|
13
|
+
type ModelEntityRelations = {
|
|
14
|
+
ancestors: string[][]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
// Map of operations available on an entity
|
|
19
|
+
type ModelOpMap = Partial<Record<OpName, ModelOp | undefined>>
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// Field-specific operation configuration
|
|
23
|
+
type ModelFieldOp = {
|
|
24
|
+
type: any // @voxgig/struct validation schema
|
|
25
|
+
req: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// Entity field definition
|
|
30
|
+
type ModelField = {
|
|
31
|
+
name: string
|
|
32
|
+
type: any // @voxgig/struct validation schema
|
|
33
|
+
req: boolean
|
|
34
|
+
op: Partial<Record<OpName, ModelFieldOp>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Operation argument/parameter definition
|
|
39
|
+
type ModelArg = {
|
|
40
|
+
name: string
|
|
41
|
+
type: any // @voxgig/struct validation schema
|
|
42
|
+
kind: 'param' | 'query' | 'header' | 'cookie'
|
|
43
|
+
req: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// Alternative implementation of an operation
|
|
48
|
+
type ModelAlt = {
|
|
49
|
+
orig: string
|
|
50
|
+
method: MethodName
|
|
51
|
+
parts: string[]
|
|
52
|
+
args: Partial<{
|
|
53
|
+
param: ModelArg[]
|
|
54
|
+
query: ModelArg[]
|
|
55
|
+
header: ModelArg[]
|
|
56
|
+
cookie: ModelArg[]
|
|
57
|
+
}>
|
|
58
|
+
select: {
|
|
59
|
+
param?: Record<string, boolean | string>
|
|
60
|
+
query?: Record<string, boolean | string>
|
|
61
|
+
header?: Record<string, boolean | string>
|
|
62
|
+
cookie?: Record<string, boolean | string>
|
|
63
|
+
$action?: string
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Operation definition
|
|
69
|
+
type ModelOp = {
|
|
70
|
+
name: OpName
|
|
71
|
+
alts: ModelAlt[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Entity definition - core model entity with operations and fields
|
|
76
|
+
type ModelEntity = {
|
|
77
|
+
name: string,
|
|
78
|
+
op: ModelOpMap,
|
|
79
|
+
fields: ModelField[],
|
|
80
|
+
id: {
|
|
81
|
+
name: string,
|
|
82
|
+
field: string,
|
|
83
|
+
},
|
|
84
|
+
relations: ModelEntityRelations
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export type {
|
|
89
|
+
OpName,
|
|
90
|
+
ModelEntityRelations,
|
|
91
|
+
ModelOpMap,
|
|
92
|
+
ModelFieldOp,
|
|
93
|
+
ModelField,
|
|
94
|
+
ModelArg,
|
|
95
|
+
ModelAlt,
|
|
96
|
+
ModelOp,
|
|
97
|
+
ModelEntity,
|
|
98
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
/* Copyright (c) 2024 Voxgig, MIT License */
|
|
1
|
+
/* Copyright (c) 2024-2025 Voxgig, MIT License */
|
|
2
2
|
|
|
3
3
|
import { bundleFromString, createConfig } from '@redocly/openapi-core'
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { depluralize } from './utility'
|
|
5
|
+
import decircular from 'decircular'
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
// Parse an API definition source into a JSON sructure.
|
|
@@ -59,16 +56,17 @@ async function parseOpenAPI(source: any, meta?: any) {
|
|
|
59
56
|
addXRefs(bundleWithRefs.bundle.parsed)
|
|
60
57
|
|
|
61
58
|
// Serialize back to string with x-refs preserved
|
|
62
|
-
const sourceWithXRefs = JSON.stringify(bundleWithRefs.bundle.parsed)
|
|
59
|
+
const sourceWithXRefs = JSON.stringify(decircular(bundleWithRefs.bundle.parsed))
|
|
63
60
|
|
|
64
61
|
// Second pass: parse with dereferencing
|
|
65
62
|
const bundle = await bundleFromString({
|
|
66
63
|
source: sourceWithXRefs,
|
|
64
|
+
// source,
|
|
67
65
|
config,
|
|
68
66
|
dereference: true,
|
|
69
67
|
})
|
|
70
68
|
|
|
71
|
-
const def = bundle.bundle.parsed
|
|
69
|
+
const def = decircular(bundle.bundle.parsed)
|
|
72
70
|
|
|
73
71
|
return def
|
|
74
72
|
}
|
|
@@ -79,60 +77,6 @@ async function parseOpenAPI(source: any, meta?: any) {
|
|
|
79
77
|
|
|
80
78
|
|
|
81
79
|
|
|
82
|
-
// Make consistent changes to support semantic entities.
|
|
83
|
-
function rewrite(def: any) {
|
|
84
|
-
const paths = def.paths
|
|
85
|
-
each(paths, (path) => {
|
|
86
|
-
each(path.parameters, (param: any) => {
|
|
87
|
-
|
|
88
|
-
let new_param = param.name
|
|
89
|
-
let new_path = path.key$
|
|
90
|
-
|
|
91
|
-
// Rename param if nane is "id", and not the final param.
|
|
92
|
-
// Rewrite /foo/{id}/bar as /foo/{foo_id}/bar.
|
|
93
|
-
// Avoids ambiguity with bar id.
|
|
94
|
-
if (param.name.match(/^id$/i)) {
|
|
95
|
-
let m = path.key$.match(/\/([^\/]+)\/{id\}\/[^\/]/)
|
|
96
|
-
|
|
97
|
-
if (m) {
|
|
98
|
-
const parent = depluralize(snakify(m[1]))
|
|
99
|
-
new_param = `${parent}_id`
|
|
100
|
-
new_path = path.key$.replace('{id}', '{' + new_param + '}')
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
new_param = depluralize(snakify(param.name))
|
|
105
|
-
new_path = path.key$.replace('{' + param.name + '}', '{' + new_param + '}')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let pathdef = paths[path.key$]
|
|
109
|
-
delete paths[path.key$]
|
|
110
|
-
|
|
111
|
-
paths[new_path] = pathdef
|
|
112
|
-
path.key$ = new_path
|
|
113
|
-
|
|
114
|
-
param.name = new_param
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
sortkeys(def, 'paths')
|
|
120
|
-
sortkeys(def, 'components')
|
|
121
|
-
|
|
122
|
-
return def
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
function sortkeys(obj: any, prop: string) {
|
|
127
|
-
const sorted: any = {}
|
|
128
|
-
const sorted_keys = Object.keys(obj[prop]).sort()
|
|
129
|
-
for (let sk of sorted_keys) {
|
|
130
|
-
sorted[sk] = obj[prop][sk]
|
|
131
|
-
}
|
|
132
|
-
obj[prop] = sorted
|
|
133
|
-
}
|
|
134
|
-
|
|
135
80
|
export {
|
|
136
81
|
parse,
|
|
137
|
-
rewrite,
|
|
138
82
|
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
type JSONSchema = Record<string, any>;
|
|
2
|
+
|
|
3
|
+
type ConvertOptions = {
|
|
4
|
+
/** Where to resolve $ref from. Use your OpenAPI doc’s components.schemas, or JSON Schema $defs. */
|
|
5
|
+
refRoots?: Array<Record<string, JSONSchema>>;
|
|
6
|
+
/** Optional: pass the entire root doc; only used if your $ref are absolute like #/components/schemas/X */
|
|
7
|
+
rootDoc?: Record<string, any>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TYPE_TOKEN: Record<string, string> = {
|
|
11
|
+
string: "$STRING",
|
|
12
|
+
number: "$NUMBER",
|
|
13
|
+
integer: "$INTEGER",
|
|
14
|
+
boolean: "$BOOLEAN",
|
|
15
|
+
null: "$NULL",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function convert(
|
|
19
|
+
schema: JSONSchema | undefined,
|
|
20
|
+
opts: ConvertOptions = {}
|
|
21
|
+
): any {
|
|
22
|
+
const seen = new Set<object>();
|
|
23
|
+
const resolvingRefs = new Set<string>();
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const resolveRef = ($ref: string): JSONSchema | undefined => {
|
|
27
|
+
if (!$ref.startsWith("#/")) return undefined; // non-local refs not supported per spec; return undefined to fallback
|
|
28
|
+
const parts = $ref.slice(2).split("/").map(unescapeRefToken);
|
|
29
|
+
let node: any = opts.rootDoc ?? {};
|
|
30
|
+
for (const p of parts) node = node?.[p];
|
|
31
|
+
if (node) return node;
|
|
32
|
+
|
|
33
|
+
// Try common roots passed in refRoots (e.g., components.schemas, $defs)
|
|
34
|
+
for (const root of opts.refRoots ?? []) {
|
|
35
|
+
let probe: any = root;
|
|
36
|
+
for (const p of parts) probe = probe?.[p];
|
|
37
|
+
if (probe) return probe;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const expandRef = (sch: JSONSchema): JSONSchema => {
|
|
43
|
+
if (typeof sch?.$ref === "string") {
|
|
44
|
+
const target = resolveRef(sch.$ref);
|
|
45
|
+
if (target) return target;
|
|
46
|
+
// Not supported → per rule 10, “just expand the reference”; if we cannot, fall back to ANY.
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
return sch;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function convert(s: JSONSchema | undefined): any {
|
|
53
|
+
if (!s || Object.keys(s).length === 0) {
|
|
54
|
+
// Rule 13 – empty schema accepts anything
|
|
55
|
+
return "$ANY";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prevent cycles from blowing the stack on malformed docs
|
|
59
|
+
if (seen.has(s)) return "$ANY";
|
|
60
|
+
seen.add(s);
|
|
61
|
+
|
|
62
|
+
// // // Expand $ref immediately (Rule 10)
|
|
63
|
+
// // if (s.$ref) {
|
|
64
|
+
// // const tgt = expandRef(s);
|
|
65
|
+
// // // Merge local decorations on top of the ref target (common in OAS)
|
|
66
|
+
// // const merged = { ...tgt, ...without(s, ["$ref"]) };
|
|
67
|
+
// // const out = convert(merged);
|
|
68
|
+
// // seen.delete(s);
|
|
69
|
+
// // return out;
|
|
70
|
+
// // }
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// // Expand $ref immediately (Rule 10)
|
|
74
|
+
// if (s.$ref) {
|
|
75
|
+
// const target = expandRef(s);
|
|
76
|
+
// const localDecor = without(s, ["$ref"]); // local fields that decorate the ref
|
|
77
|
+
// // Use the same semantics as allOf merging to deep-merge properties/openness/etc.
|
|
78
|
+
// const merged = mergeAllOf([target, localDecor]);
|
|
79
|
+
// const out = convert(merged);
|
|
80
|
+
// seen.delete(s);
|
|
81
|
+
// return out;
|
|
82
|
+
// }
|
|
83
|
+
|
|
84
|
+
if (s.$ref) {
|
|
85
|
+
const ref = String(s.$ref);
|
|
86
|
+
if (resolvingRefs.has(ref)) {
|
|
87
|
+
// cycle detected -> cannot expand, fall back to ANY
|
|
88
|
+
seen.delete(s);
|
|
89
|
+
return "$ANY";
|
|
90
|
+
}
|
|
91
|
+
resolvingRefs.add(ref);
|
|
92
|
+
|
|
93
|
+
const target = expandRef(s); // returns referenced schema or {}
|
|
94
|
+
const localDecor = without(s, ["$ref"]);
|
|
95
|
+
// merge like allOf so local decorations augment target
|
|
96
|
+
const merged = mergeAllOf([target, localDecor]);
|
|
97
|
+
|
|
98
|
+
const out = convert(merged);
|
|
99
|
+
|
|
100
|
+
resolvingRefs.delete(ref);
|
|
101
|
+
seen.delete(s);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// Composition
|
|
107
|
+
if (Array.isArray(s.allOf) && s.allOf.length > 0) {
|
|
108
|
+
const merged = mergeAllOf(s.allOf.map((x) => expandRef(x)));
|
|
109
|
+
// Carry over top-level decorations (e.g., nullable, annotations)
|
|
110
|
+
const mergedWithTop = { ...merged, ...without(s, ["allOf"]) };
|
|
111
|
+
const out = convert(mergedWithTop);
|
|
112
|
+
seen.delete(s);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(s.oneOf) && s.oneOf.length > 0) {
|
|
117
|
+
const alts = s.oneOf.map((x) => convert(expandRef(x)));
|
|
118
|
+
const out = ["$ONE", ...alts];
|
|
119
|
+
seen.delete(s);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(s.anyOf) && s.anyOf.length > 0) {
|
|
124
|
+
const alts = s.anyOf.map((x) => convert(expandRef(x)));
|
|
125
|
+
const out = ["$ANY", ...alts]; // Rule 3 – anyOf uses "$ANY" in the same directive position
|
|
126
|
+
seen.delete(s);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Enum / const → $EXACT
|
|
131
|
+
if (Array.isArray(s.enum) && s.enum.length > 0) {
|
|
132
|
+
const out = ["$EXACT", ...s.enum];
|
|
133
|
+
seen.delete(s);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(s, "const")) {
|
|
137
|
+
const out = ["$EXACT", s.const];
|
|
138
|
+
seen.delete(s);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle OpenAPI nullable at this level by wrapping the base type later
|
|
143
|
+
const nullable = s.nullable === true;
|
|
144
|
+
|
|
145
|
+
// Type handling (could be string or array)
|
|
146
|
+
const t = s.type;
|
|
147
|
+
if (Array.isArray(t) && t.length > 0) {
|
|
148
|
+
// Union of primitives (and possibly null)
|
|
149
|
+
const tokens = t.map((tt) => TYPE_TOKEN[tt] ?? "$ANY");
|
|
150
|
+
const out = ["$ONE", ...tokens];
|
|
151
|
+
seen.delete(s);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Objects
|
|
156
|
+
if (t === "object" || s.properties || s.additionalProperties !== undefined) {
|
|
157
|
+
const obj: Record<string, any> = {};
|
|
158
|
+
|
|
159
|
+
// $OPEN
|
|
160
|
+
if (s.additionalProperties === true) {
|
|
161
|
+
obj["$OPEN"] = true;
|
|
162
|
+
} else if (s.additionalProperties && typeof s.additionalProperties === "object") {
|
|
163
|
+
// We cannot express typed extras (Rule 6). Keep it open but untyped.
|
|
164
|
+
obj["$OPEN"] = true;
|
|
165
|
+
}
|
|
166
|
+
// $NOTE annotations (Rule 11)
|
|
167
|
+
const note: Record<string, any> = {};
|
|
168
|
+
if (s.readOnly === true) note.readOnly = true;
|
|
169
|
+
if (s.writeOnly === true) note.writeOnly = true;
|
|
170
|
+
if (s.deprecated === true) note.deprecated = true;
|
|
171
|
+
if (Object.keys(note).length > 0) obj["$NOTE"] = note;
|
|
172
|
+
|
|
173
|
+
const props = s.properties ?? {};
|
|
174
|
+
for (const [key, sub] of Object.entries<JSONSchema>(props)) {
|
|
175
|
+
const converted = convert(expandRef(sub));
|
|
176
|
+
const isNullable =
|
|
177
|
+
sub?.nullable === true ||
|
|
178
|
+
(Array.isArray(sub?.type) && sub.type.includes("null")) ||
|
|
179
|
+
includesNullViaOneAnyOf(sub);
|
|
180
|
+
|
|
181
|
+
obj[key] = isNullable ? wrapNullable(converted) : converted;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
seen.delete(s);
|
|
185
|
+
return obj;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Arrays
|
|
189
|
+
if (t === "array" || s.items) {
|
|
190
|
+
const items = s.items;
|
|
191
|
+
if (Array.isArray(items)) {
|
|
192
|
+
// Tuple validation → positional sub-schemas
|
|
193
|
+
const out = items.map((it) => convert(expandRef(it)));
|
|
194
|
+
seen.delete(s);
|
|
195
|
+
return out;
|
|
196
|
+
} else if (items && typeof items === "object") {
|
|
197
|
+
// Homogeneous array → ["$CHILD", sub]
|
|
198
|
+
const out = ["$CHILD", convert(expandRef(items))];
|
|
199
|
+
seen.delete(s);
|
|
200
|
+
return out;
|
|
201
|
+
} else {
|
|
202
|
+
// No items → accept any child element
|
|
203
|
+
seen.delete(s);
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Primitives
|
|
209
|
+
if (typeof t === "string" && TYPE_TOKEN[t]) {
|
|
210
|
+
const token = TYPE_TOKEN[t];
|
|
211
|
+
const base = token;
|
|
212
|
+
|
|
213
|
+
const out = nullable ? wrapNullable(base) : base;
|
|
214
|
+
seen.delete(s);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// No explicit type, but we might still infer:
|
|
219
|
+
if (s.properties || s.additionalProperties !== undefined) {
|
|
220
|
+
// already handled in object branch, but keep a guard
|
|
221
|
+
const out = convert({ ...s, type: "object" });
|
|
222
|
+
seen.delete(s);
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
if (s.items) {
|
|
226
|
+
const out = convert({ ...s, type: "array" });
|
|
227
|
+
seen.delete(s);
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback
|
|
232
|
+
seen.delete(s);
|
|
233
|
+
return "$ANY";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = convert(schema);
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* -------------------------- helpers -------------------------- */
|
|
241
|
+
|
|
242
|
+
function unescapeRefToken(s: string) {
|
|
243
|
+
// JSON Pointer ~0 -> ~, ~1 -> /
|
|
244
|
+
return s.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function without<T extends Record<string, any>>(obj: T, keys: string[]): T {
|
|
248
|
+
const copy: any = {};
|
|
249
|
+
for (const k of Object.keys(obj)) if (!keys.includes(k)) copy[k] = obj[k];
|
|
250
|
+
return copy;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function includesNullViaOneAnyOf(s: JSONSchema): boolean {
|
|
254
|
+
const hasNullIn = (arr?: any[]) =>
|
|
255
|
+
Array.isArray(arr) &&
|
|
256
|
+
arr.some((x) => x?.type === "null" || (Array.isArray(x?.type) && x.type.includes("null")));
|
|
257
|
+
return hasNullIn(s.oneOf) || hasNullIn(s.anyOf);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function wrapNullable(inner: any) {
|
|
261
|
+
// Rule 1/4 – nullable → ["$ONE", inner, "$NULL"]
|
|
262
|
+
return Array.isArray(inner) && inner[0] === "$ONE"
|
|
263
|
+
? inner.includes("$NULL")
|
|
264
|
+
? inner
|
|
265
|
+
: ["$ONE", ...inner.slice(1), "$NULL"]
|
|
266
|
+
: ["$ONE", inner, "$NULL"];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function mergeAllOf(schemas: JSONSchema[]): JSONSchema {
|
|
270
|
+
const out: JSONSchema = {};
|
|
271
|
+
let anyObject = false;
|
|
272
|
+
|
|
273
|
+
for (const s of schemas) {
|
|
274
|
+
const schema = s || {};
|
|
275
|
+
|
|
276
|
+
if (schema.type === "object" || schema.properties || schema.additionalProperties !== undefined) {
|
|
277
|
+
anyObject = true;
|
|
278
|
+
out.type = "object";
|
|
279
|
+
|
|
280
|
+
// --- properties: merge without losing earlier branches ---
|
|
281
|
+
const srcProps = schema.properties ?? {};
|
|
282
|
+
const dstProps = (out.properties = out.properties ?? {});
|
|
283
|
+
for (const [k, v] of Object.entries(srcProps)) {
|
|
284
|
+
if (k in dstProps) {
|
|
285
|
+
// Combine the two property schemas using allOf semantics.
|
|
286
|
+
// This preserves earlier constraints like {type:'number'} and
|
|
287
|
+
// later decorations like {nullable:true}.
|
|
288
|
+
dstProps[k] = { allOf: [dstProps[k], v] };
|
|
289
|
+
} else {
|
|
290
|
+
dstProps[k] = v;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- additionalProperties: openness is "true if any true/object" ---
|
|
295
|
+
const ap = schema.additionalProperties;
|
|
296
|
+
if (ap === true || (ap && typeof ap === "object")) {
|
|
297
|
+
out.additionalProperties = true;
|
|
298
|
+
} else if (out.additionalProperties === undefined) {
|
|
299
|
+
out.additionalProperties = ap;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- required: union (not used in voxgig/struct but harmless to keep) ---
|
|
303
|
+
if (Array.isArray(schema.required)) {
|
|
304
|
+
const req = new Set([...(out.required ?? []), ...schema.required]);
|
|
305
|
+
out.required = Array.from(req);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// For non-object shapes in allOf, a shallow merge is the best we can do.
|
|
309
|
+
Object.assign(out, schema);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If nothing was objecty, return a shallow merge across all schemas.
|
|
314
|
+
return anyObject
|
|
315
|
+
? out
|
|
316
|
+
: (schemas.reduce((a, b) => ({ ...a, ...(b || {}) }), {}) as JSONSchema);
|
|
317
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { each, snakify } from 'jostraca'
|
|
4
|
+
|
|
5
|
+
import type { TransformResult, Transform } from '../transform'
|
|
6
|
+
|
|
7
|
+
import { formatJSONIC, depluralize, validator } from '../utility'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import { KIT } from '../types'
|
|
11
|
+
|
|
12
|
+
import type { KitModel } from '../types'
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
PathDef,
|
|
16
|
+
ParameterDef,
|
|
17
|
+
MethodDef,
|
|
18
|
+
} from '../def'
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
OpName,
|
|
22
|
+
ModelOp,
|
|
23
|
+
ModelEntity,
|
|
24
|
+
ModelAlt,
|
|
25
|
+
ModelArg,
|
|
26
|
+
} from '../model'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const argsTransform = async function(
|
|
31
|
+
ctx: any,
|
|
32
|
+
): Promise<TransformResult> {
|
|
33
|
+
const { apimodel, def } = ctx
|
|
34
|
+
const kit: KitModel = apimodel.main[KIT]
|
|
35
|
+
|
|
36
|
+
let msg = 'args '
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
each(kit.entity, (ment: ModelEntity, entname: string) => {
|
|
40
|
+
each(ment.op, (mop: ModelOp, opname: OpName) => {
|
|
41
|
+
each(mop.alts, (malt: ModelAlt) => {
|
|
42
|
+
const argdefs: ParameterDef[] = []
|
|
43
|
+
|
|
44
|
+
const pathdef: PathDef = def.paths[malt.orig]
|
|
45
|
+
argdefs.push(...(pathdef.parameters ?? []))
|
|
46
|
+
|
|
47
|
+
const opdef: MethodDef = (pathdef as any)[malt.method.toLowerCase()]
|
|
48
|
+
argdefs.push(...(opdef.parameters ?? []))
|
|
49
|
+
|
|
50
|
+
resolveArgs(ment, mop, malt, argdefs)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
msg += ment.name + ' '
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { ok: true, msg }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const ARG_KIND: Record<string, ModelArg["kind"]> = {
|
|
62
|
+
'query': 'query',
|
|
63
|
+
'header': 'header',
|
|
64
|
+
'path': 'param',
|
|
65
|
+
'cookie': 'cookie',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
function resolveArgs(ment: ModelEntity, mop: ModelOp, malt: ModelAlt, argdefs: ParameterDef[]) {
|
|
70
|
+
each(argdefs, (argdef: ParameterDef) => {
|
|
71
|
+
const marg: ModelArg = {
|
|
72
|
+
name: depluralize(snakify(argdef.name)),
|
|
73
|
+
type: validator(argdef.schema?.type),
|
|
74
|
+
kind: ARG_KIND[argdef.in] ?? 'query',
|
|
75
|
+
req: !!argdef.required
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (argdef.nullable) {
|
|
79
|
+
marg.type = ['`$ONE`', '`$NULL`', marg.type]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// insert sorted by name
|
|
83
|
+
let kindargs = (malt.args[marg.kind] = malt.args[marg.kind] ?? [])
|
|
84
|
+
|
|
85
|
+
let kalen = kindargs.length
|
|
86
|
+
for (let ka, i = 0; i <= kalen; i++) {
|
|
87
|
+
ka = kindargs[i]
|
|
88
|
+
if (ka && ka.name > marg.name) {
|
|
89
|
+
kindargs = [...kindargs.slice(0, i), marg, ...kindargs.slice(i + 1)]
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
kindargs.push(marg)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
argsTransform,
|
|
102
|
+
}
|
package/src/transform/clean.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
import { each, getx } from 'jostraca'
|
|
3
|
-
|
|
4
2
|
import type { TransformResult } from '../transform'
|
|
5
3
|
|
|
6
|
-
import { walk } from '@voxgig/struct'
|
|
4
|
+
import { walk, isempty, isnode, ismap, islist } from '@voxgig/struct'
|
|
7
5
|
|
|
6
|
+
import { formatJSONIC } from '../utility'
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
const cleanTransform = async function(
|
|
@@ -12,12 +11,48 @@ const cleanTransform = async function(
|
|
|
12
11
|
): Promise<TransformResult> {
|
|
13
12
|
const { apimodel } = ctx
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
let cur: any[] = []
|
|
15
|
+
|
|
16
|
+
// Remove empty nodes and undefined values
|
|
17
|
+
walk(
|
|
18
|
+
apimodel,
|
|
19
|
+
(k: any, v: any, _p: any, ancestors: any) => {
|
|
20
|
+
if (undefined === k) {
|
|
21
|
+
cur[ancestors.length] = ismap(v) ? {} : islist(v) ? [] : v
|
|
22
|
+
return v
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let vi = v
|
|
26
|
+
|
|
27
|
+
if (isnode(v)) {
|
|
28
|
+
if (isempty(v)) {
|
|
29
|
+
vi = undefined
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
vi = cur[ancestors.length] = ismap(v) ? {} : []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (undefined !== vi && !k.endsWith('$')) {
|
|
38
|
+
cur[ancestors.length - 1][k] = vi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return v
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
(k: any, _v: any, _p: any, ancestors: any) => {
|
|
45
|
+
const pi = cur[ancestors.length - 1]
|
|
46
|
+
if (undefined !== pi) {
|
|
47
|
+
const vi = pi[k]
|
|
48
|
+
if (isnode(vi) && isempty(vi)) {
|
|
49
|
+
delete pi[k]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
18
52
|
}
|
|
19
|
-
|
|
20
|
-
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ctx.apimodel = cur[0]
|
|
21
56
|
|
|
22
57
|
return { ok: true, msg: 'clean' }
|
|
23
58
|
}
|