@voxgig/sdkgen 4.30.3 → 4.31.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/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/ReadmeTop.js +6 -5
- package/dist/cmp/ReadmeTop.js.map +1 -1
- package/dist/helpers/definitionPlan.d.ts +27 -0
- package/dist/helpers/definitionPlan.js +219 -0
- package/dist/helpers/definitionPlan.js.map +1 -0
- package/dist/sdkgen.d.ts +2 -1
- package/dist/sdkgen.js +5 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/project/.sdk/src/cmp/c/PrepareAuth_c.ts +5 -3
- package/project/.sdk/src/cmp/clojure/PrepareAuth_clojure.ts +7 -5
- package/project/.sdk/src/cmp/cpp/PrepareAuth_cpp.ts +5 -3
- package/project/.sdk/src/cmp/csharp/PrepareAuth_csharp.ts +6 -4
- package/project/.sdk/src/cmp/elixir/PrepareAuth_elixir.ts +8 -6
- package/project/.sdk/src/cmp/go/PrepareAuth_go.ts +7 -9
- package/project/.sdk/src/cmp/java/PrepareAuth_java.ts +5 -3
- package/project/.sdk/src/cmp/js/PrepareAuth_js.ts +6 -4
- package/project/.sdk/src/cmp/js/ReadmeInstall_js.ts +22 -5
- package/project/.sdk/src/cmp/js/TestDefinition_js.ts +40 -0
- package/project/.sdk/src/cmp/js/Test_js.ts +2 -0
- package/project/.sdk/src/cmp/kotlin/PrepareAuth_kotlin.ts +5 -3
- package/project/.sdk/src/cmp/lua/PrepareAuth_lua.ts +7 -3
- package/project/.sdk/src/cmp/ocaml/PrepareAuth_ocaml.ts +7 -5
- package/project/.sdk/src/cmp/perl/PrepareAuth_perl.ts +6 -3
- package/project/.sdk/src/cmp/php/PrepareAuth_php.ts +5 -3
- package/project/.sdk/src/cmp/php/ReadmeInstall_php.ts +11 -5
- package/project/.sdk/src/cmp/py/PrepareAuth_py.ts +5 -3
- package/project/.sdk/src/cmp/rb/PrepareAuth_rb.ts +5 -3
- package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +14 -4
- package/project/.sdk/src/cmp/rust/PrepareAuth_rust.ts +5 -3
- package/project/.sdk/src/cmp/scala/PrepareAuth_scala.ts +5 -3
- package/project/.sdk/src/cmp/swift/PrepareAuth_swift.ts +5 -3
- package/project/.sdk/src/cmp/ts/PrepareAuth_ts.ts +6 -4
- package/project/.sdk/src/cmp/ts/ReadmeInstall_ts.ts +10 -4
- package/project/.sdk/src/cmp/ts/TestDefinition_ts.ts +40 -0
- package/project/.sdk/src/cmp/ts/Test_ts.ts +2 -0
- package/project/.sdk/src/cmp/zig/PrepareAuth_zig.ts +5 -3
- package/project/.sdk/tm/c/utility/prepare_query.c +34 -2
- package/project/.sdk/tm/clojure/src/sdk/core.clj +20 -2
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +31 -2
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +32 -4
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +35 -1
- package/project/.sdk/tm/go/utility/prepare_query.go +31 -1
- package/project/.sdk/tm/java/utility/PrepareQuery.java +29 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +16 -2
- package/project/.sdk/tm/js/test/definition-runner.js +161 -0
- package/project/.sdk/tm/js/test/pipeline.test.js +43 -3
- package/project/.sdk/tm/js/test/sdk-test-control.json +4 -1
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +29 -5
- package/project/.sdk/tm/lua/utility/prepare_query.lua +31 -2
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +22 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +25 -2
- package/project/.sdk/tm/php/utility/PrepareQuery.php +28 -1
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +23 -2
- package/project/.sdk/tm/rb/utility/prepare_query.rb +24 -2
- package/project/.sdk/tm/rust/utility/prepare_query.rs +34 -2
- package/project/.sdk/tm/scala/utility/Prepare.scala +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +23 -5
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +16 -2
- package/project/.sdk/tm/ts/test/definition-runner.ts +179 -0
- package/project/.sdk/tm/ts/test/pipeline.test.ts +43 -3
- package/project/.sdk/tm/ts/test/sdk-test-control.json +4 -1
- package/project/.sdk/tm/ts/test/utility.ts +3 -2
- package/project/.sdk/tm/zig/core/utility.zig +29 -1
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/ReadmeTop.ts +7 -6
- package/src/helpers/definitionPlan.ts +265 -0
- package/src/sdkgen.ts +2 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Voxgig Ltd, MIT License */
|
|
2
|
+
|
|
3
|
+
import { KIT, nom } from '@voxgig/apidef'
|
|
4
|
+
|
|
5
|
+
import { entityCollection } from './opShape'
|
|
6
|
+
import { pointSegments } from './pointPath'
|
|
7
|
+
import { pointFacts } from './resolved'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// What a generated definition test checks an operation against: the API
|
|
11
|
+
// definition's own facts, never the model inferred from it. A test generated
|
|
12
|
+
// from the model agrees with the model, so it cannot see a wrong unwrap, a
|
|
13
|
+
// dropped credential or a path parameter echoed into the query.
|
|
14
|
+
|
|
15
|
+
type Credential = { in: string, name: string, scheme?: string }
|
|
16
|
+
|
|
17
|
+
type DefinitionPoint = {
|
|
18
|
+
entity: string
|
|
19
|
+
accessor: string
|
|
20
|
+
op: string
|
|
21
|
+
method: string
|
|
22
|
+
path: string
|
|
23
|
+
action?: string
|
|
24
|
+
args: { name: string, wire: string, value: any }[]
|
|
25
|
+
select: Record<string, any>
|
|
26
|
+
query: string[]
|
|
27
|
+
auth: Credential[][] | null
|
|
28
|
+
status: number
|
|
29
|
+
sample: any
|
|
30
|
+
idField: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const MAX_ITEMS = 3
|
|
35
|
+
const MAX_DEPTH = 8
|
|
36
|
+
const MAX_SAMPLE = 32 * 1024
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function definitionPlan(ctx$: any): DefinitionPoint[] {
|
|
40
|
+
const model = ctx$?.model
|
|
41
|
+
const plan: DefinitionPoint[] = []
|
|
42
|
+
const unchecked = false === model?.main?.[KIT]?.config?.auth?.active ||
|
|
43
|
+
false === model?.main?.[KIT]?.info?.auth
|
|
44
|
+
|
|
45
|
+
for (const entity of Object.values(entityCollection(model)) as any[]) {
|
|
46
|
+
if (false === entity.active) continue
|
|
47
|
+
|
|
48
|
+
for (const [op, operation] of Object.entries(entity.op || {}) as any[]) {
|
|
49
|
+
// An inactive operation stays in the model but gets no method.
|
|
50
|
+
if (false === operation?.active) continue
|
|
51
|
+
|
|
52
|
+
const points = (operation?.points || []).filter((p: any) => false !== p.a)
|
|
53
|
+
|
|
54
|
+
for (const point of points) {
|
|
55
|
+
if ('graphql' === point.k) continue
|
|
56
|
+
|
|
57
|
+
const facts = pointFacts(ctx$, point)
|
|
58
|
+
if ('http' !== facts?.protocol) continue
|
|
59
|
+
|
|
60
|
+
// Points the SDK cannot tell apart are selected by order, not input.
|
|
61
|
+
const select = point.q || {}
|
|
62
|
+
const same = points.filter((p: any) =>
|
|
63
|
+
JSON.stringify(p.q || {}) === JSON.stringify(select))
|
|
64
|
+
if (1 !== same.length) continue
|
|
65
|
+
|
|
66
|
+
const params: any[] = Array.isArray(facts.parameters) ? facts.parameters : []
|
|
67
|
+
const placed = pathPlaceholders(point)
|
|
68
|
+
const args = (point.g?.params || []).map((arg: any, i: number) => {
|
|
69
|
+
const wire = placed[arg.n] ?? (arg.or || arg.n)
|
|
70
|
+
const def = params.find((p: any) => 'path' === p?.in && wire === p?.name)
|
|
71
|
+
return { name: arg.n, wire, value: scalar(def?.example ?? def?.schema?.example) ?? 'p' + (i + 1) }
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const selected: Record<string, any> = {}
|
|
75
|
+
for (const key of select.exist || []) {
|
|
76
|
+
if (args.some((a: any) => a.name === key)) continue
|
|
77
|
+
const def = params.find((p: any) => key === p?.name)
|
|
78
|
+
selected[key] = scalar(def?.example ?? def?.schema?.example) ?? 'v1'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Every query argument is sent too, so a name that goes out in the
|
|
82
|
+
// model's spelling fails even where no point selects on it. One that
|
|
83
|
+
// another point selects on would move the SDK to that point.
|
|
84
|
+
const elsewhere = new Set(points.filter((p: any) => p !== point)
|
|
85
|
+
.flatMap((p: any) => p.q?.exist || []))
|
|
86
|
+
for (const arg of point.g?.query || []) {
|
|
87
|
+
if (false === arg.a || undefined !== selected[arg.n] || elsewhere.has(arg.n)) continue
|
|
88
|
+
const def = params.find((p: any) => 'query' === p?.in && (arg.or || arg.n) === p?.name)
|
|
89
|
+
selected[arg.n] = scalar(arg.ex ?? def?.example ?? def?.schema?.example) ?? 'v1'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const success = successResponse(facts.responses)
|
|
93
|
+
const media = null == success ? undefined : jsonMedia(success.response)
|
|
94
|
+
|
|
95
|
+
plan.push({
|
|
96
|
+
entity: entity.name,
|
|
97
|
+
accessor: nom(entity, 'Name'),
|
|
98
|
+
op,
|
|
99
|
+
method: String(point.m).toUpperCase(),
|
|
100
|
+
path: point.o,
|
|
101
|
+
...(null == select.$action ? {} : { action: select.$action }),
|
|
102
|
+
args,
|
|
103
|
+
select: selected,
|
|
104
|
+
query: params.filter((p: any) => 'query' === p?.in).map((p: any) => p.name),
|
|
105
|
+
auth: unchecked ? null : credentialSets(facts),
|
|
106
|
+
status: success?.status ?? 200,
|
|
107
|
+
sample: null == media ? null : boundedSample(sampleOf(media)),
|
|
108
|
+
idField: entity.id?.field || 'id',
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return plan
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
// The definition's name for each path parameter, keyed by the model's. The
|
|
119
|
+
// model may rename a placeholder, but it keeps the placeholder's place in the
|
|
120
|
+
// path, so position pairs each with its model name whatever `or` holds.
|
|
121
|
+
function pathPlaceholders(point: any): Record<string, string> {
|
|
122
|
+
const orig = String(point.o || '').split('/').filter((part) => '' !== part)
|
|
123
|
+
const segments = pointSegments(point)
|
|
124
|
+
const out: Record<string, string> = {}
|
|
125
|
+
if (orig.length !== segments.length) return out
|
|
126
|
+
segments.forEach((seg: any, i: number) => {
|
|
127
|
+
const m = /^\{([^}]+)\}$/.exec(orig[i])
|
|
128
|
+
if (null != seg.var && null != m) out[seg.var] = m[1]
|
|
129
|
+
})
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
function scalar(value: any): any {
|
|
135
|
+
return 'string' === typeof value || 'number' === typeof value ? value : undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
function successResponse(responses: any): { status: number, response: any } | undefined {
|
|
140
|
+
if (null == responses || 'object' !== typeof responses) return undefined
|
|
141
|
+
const code = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0] ??
|
|
142
|
+
(null != responses['2XX'] ? '2XX' : undefined)
|
|
143
|
+
return null == code ? undefined :
|
|
144
|
+
{ status: '2XX' === code ? 200 : Number(code), response: responses[code] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function jsonMedia(response: any): any {
|
|
149
|
+
const content = response?.content
|
|
150
|
+
if (null != content && 'object' === typeof content) {
|
|
151
|
+
const type = Object.keys(content).find((t) => /json/i.test(t) || '*/*' === t)
|
|
152
|
+
return null == type ? undefined : content[type]
|
|
153
|
+
}
|
|
154
|
+
// Swagger puts the schema and examples on the response itself.
|
|
155
|
+
if (null != response?.schema || null != response?.examples) {
|
|
156
|
+
return { schema: response.schema, example: response.examples?.['application/json'] }
|
|
157
|
+
}
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
function sampleOf(media: any): any {
|
|
163
|
+
if (undefined !== media.example) return media.example
|
|
164
|
+
const named = Object.values(media.examples || {}).find((e: any) => undefined !== e?.value) as any
|
|
165
|
+
if (null != named) return named.value
|
|
166
|
+
if (undefined !== media.schema?.example) return media.schema.example
|
|
167
|
+
return synthesize(media.schema, 0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
// Schema-shaped data where the definition gives no example: every property,
|
|
172
|
+
// one item per array, the first branch of a union.
|
|
173
|
+
function synthesize(schema: any, depth: number): any {
|
|
174
|
+
if (null == schema || 'object' !== typeof schema || depth > 6) return undefined
|
|
175
|
+
if (undefined !== schema.example) return schema.example
|
|
176
|
+
if (Array.isArray(schema.enum) && 0 < schema.enum.length) return schema.enum[0]
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(schema.allOf)) {
|
|
179
|
+
const parts = schema.allOf.map((s: any) => synthesize(s, depth + 1))
|
|
180
|
+
.filter((v: any) => null != v && 'object' === typeof v && !Array.isArray(v))
|
|
181
|
+
return Object.assign({}, ...parts)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const union = schema.oneOf ?? schema.anyOf
|
|
185
|
+
if (Array.isArray(union) && 0 < union.length) return synthesize(union[0], depth + 1)
|
|
186
|
+
|
|
187
|
+
const type = Array.isArray(schema.type) ?
|
|
188
|
+
schema.type.find((t: string) => 'null' !== t) : schema.type
|
|
189
|
+
|
|
190
|
+
if ('array' === type || null != schema.items) {
|
|
191
|
+
const item = synthesize(schema.items, depth + 1)
|
|
192
|
+
return undefined === item ? [] : [item]
|
|
193
|
+
}
|
|
194
|
+
if ('object' === type || null != schema.properties) {
|
|
195
|
+
const out: any = {}
|
|
196
|
+
for (const [key, prop] of Object.entries(schema.properties || {})) {
|
|
197
|
+
const value = synthesize(prop, depth + 1)
|
|
198
|
+
if (undefined !== value) out[key] = value
|
|
199
|
+
}
|
|
200
|
+
return out
|
|
201
|
+
}
|
|
202
|
+
if ('integer' === type || 'number' === type) return 1
|
|
203
|
+
if ('boolean' === type) return true
|
|
204
|
+
if ('string' === type) {
|
|
205
|
+
return 'date-time' === schema.format ? '2026-01-01T00:00:00Z' :
|
|
206
|
+
'date' === schema.format ? '2026-01-01' : 'x'
|
|
207
|
+
}
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
function boundedSample(sample: any): any {
|
|
213
|
+
const bound = (node: any, depth: number): any => {
|
|
214
|
+
if (null == node || 'object' !== typeof node) return node
|
|
215
|
+
if (depth > MAX_DEPTH) return null
|
|
216
|
+
if (Array.isArray(node)) return node.slice(0, MAX_ITEMS).map((v) => bound(v, depth + 1))
|
|
217
|
+
return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, bound(v, depth + 1)]))
|
|
218
|
+
}
|
|
219
|
+
const out = bound(sample, 0)
|
|
220
|
+
return undefined === out || MAX_SAMPLE < JSON.stringify(out).length ? null : out
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// Alternatives of credentials, every one of a set needed together. Empty is a
|
|
225
|
+
// public operation; null is one whose schemes no SDK option can express.
|
|
226
|
+
function credentialSets(facts: any): Credential[][] | null {
|
|
227
|
+
const security = facts.security
|
|
228
|
+
if (!Array.isArray(security) || 0 === security.length) return []
|
|
229
|
+
if (security.some((req: any) => null == req || 0 === Object.keys(req).length)) return []
|
|
230
|
+
|
|
231
|
+
const schemes = facts.securitySchemes || {}
|
|
232
|
+
const sets: Credential[][] = []
|
|
233
|
+
for (const req of security) {
|
|
234
|
+
const set = Object.keys(req).map((name) => credentialOf(schemes[name]))
|
|
235
|
+
if (set.every((c) => null != c)) sets.push(set as Credential[])
|
|
236
|
+
}
|
|
237
|
+
return 0 === sets.length ? null : sets
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
function credentialOf(scheme: any): Credential | undefined {
|
|
242
|
+
const type = String(scheme?.type || '').toLowerCase()
|
|
243
|
+
const http = String(scheme?.scheme || '').toLowerCase()
|
|
244
|
+
|
|
245
|
+
if (('http' === type && 'basic' === http) || 'basic' === type) {
|
|
246
|
+
return { in: 'header', name: 'authorization', scheme: 'basic' }
|
|
247
|
+
}
|
|
248
|
+
if (('http' === type && 'bearer' === http) || 'oauth2' === type || 'openidconnect' === type) {
|
|
249
|
+
return { in: 'header', name: 'authorization', scheme: 'bearer' }
|
|
250
|
+
}
|
|
251
|
+
if ('apikey' === type && 'string' === typeof scheme.name &&
|
|
252
|
+
['header', 'query', 'cookie'].includes(scheme.in)) {
|
|
253
|
+
return { in: scheme.in, name: 'header' === scheme.in ? scheme.name.toLowerCase() : scheme.name }
|
|
254
|
+
}
|
|
255
|
+
return undefined
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
export type {
|
|
260
|
+
DefinitionPoint,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
definitionPlan,
|
|
265
|
+
}
|
package/src/sdkgen.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { FeatureHook } from './cmp/FeatureHook'
|
|
|
56
56
|
import { registerComponent } from './cmp/Registered'
|
|
57
57
|
|
|
58
58
|
import { resolvedFor, liveHint, pointFacts, boundedFacts, hasLiveScenarios } from './helpers/resolved'
|
|
59
|
+
import { definitionPlan } from './helpers/definitionPlan'
|
|
59
60
|
import type { RegisterOptions } from './cmp/Registered'
|
|
60
61
|
|
|
61
62
|
import { buildIdNames, entityRelationName, flowSteps } from './helpers/buildIdNames'
|
|
@@ -987,6 +988,7 @@ export {
|
|
|
987
988
|
pointFacts,
|
|
988
989
|
boundedFacts,
|
|
989
990
|
hasLiveScenarios,
|
|
991
|
+
definitionPlan,
|
|
990
992
|
|
|
991
993
|
Jostraca,
|
|
992
994
|
SdkGen,
|