@voxgig/sdkgen 4.31.1 → 4.32.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/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/definitionPlan.d.ts +5 -0
- package/dist/helpers/definitionPlan.js +44 -7
- package/dist/helpers/definitionPlan.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/c/utility/make_url.c +11 -0
- package/project/.sdk/tm/c/utility/prepare_headers.c +53 -3
- package/project/.sdk/tm/c/utility/prepare_query.c +4 -1
- package/project/.sdk/tm/c/utility/transform_request.c +31 -8
- package/project/.sdk/tm/clojure/src/sdk/core.clj +62 -15
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +72 -15
- package/project/.sdk/tm/csharp/utility/MakeUrl.cs +8 -0
- package/project/.sdk/tm/csharp/utility/PrepareHeaders.cs +34 -4
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +14 -0
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +31 -5
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +86 -24
- package/project/.sdk/tm/go/utility/make_url.go +8 -0
- package/project/.sdk/tm/go/utility/prepare_headers.go +36 -7
- package/project/.sdk/tm/go/utility/prepare_query.go +11 -0
- package/project/.sdk/tm/go/utility/transform_request.go +31 -5
- package/project/.sdk/tm/java/utility/MakeUrl.java +8 -0
- package/project/.sdk/tm/java/utility/PrepareHeaders.java +30 -6
- package/project/.sdk/tm/java/utility/PrepareQuery.java +10 -0
- package/project/.sdk/tm/java/utility/TransformRequest.java +30 -5
- package/project/.sdk/tm/js/src/Response.js +14 -1
- package/project/.sdk/tm/js/src/utility/MakeUrlUtility.js +7 -0
- package/project/.sdk/tm/js/src/utility/PrepareHeadersUtility.js +18 -0
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +3 -1
- package/project/.sdk/tm/js/src/utility/TransformRequestUtility.js +17 -3
- package/project/.sdk/tm/js/test/definition-runner.js +74 -13
- package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +25 -0
- package/project/.sdk/tm/kotlin/utility/MakeSpecUrl.kt +8 -0
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +34 -4
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -6
- package/project/.sdk/tm/lua/utility/make_url.lua +8 -0
- package/project/.sdk/tm/lua/utility/prepare_headers.lua +35 -6
- package/project/.sdk/tm/lua/utility/prepare_query.lua +10 -0
- package/project/.sdk/tm/lua/utility/transform_request.lua +37 -6
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +63 -13
- package/project/.sdk/tm/perl/utility/make_url.pm +6 -0
- package/project/.sdk/tm/perl/utility/prepare_headers.pm +21 -3
- package/project/.sdk/tm/perl/utility/prepare_query.pm +8 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +19 -7
- package/project/.sdk/tm/php/utility/MakeUrl.php +7 -0
- package/project/.sdk/tm/php/utility/PrepareHeaders.php +33 -4
- package/project/.sdk/tm/php/utility/PrepareQuery.php +11 -0
- package/project/.sdk/tm/php/utility/TransformRequest.php +28 -5
- package/project/.sdk/tm/py/pkg/utility/make_url.py +7 -0
- package/project/.sdk/tm/py/pkg/utility/prepare_headers.py +27 -6
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +8 -0
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +21 -5
- package/project/.sdk/tm/rb/utility/make_url.rb +7 -0
- package/project/.sdk/tm/rb/utility/prepare_headers.rb +21 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +8 -0
- package/project/.sdk/tm/rb/utility/transform_request.rb +18 -5
- package/project/.sdk/tm/rust/utility/make_url.rs +10 -1
- package/project/.sdk/tm/rust/utility/prepare_headers.rs +43 -7
- package/project/.sdk/tm/rust/utility/prepare_query.rs +18 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +29 -7
- package/project/.sdk/tm/scala/utility/Make.scala +9 -0
- package/project/.sdk/tm/scala/utility/Prepare.scala +42 -3
- package/project/.sdk/tm/scala/utility/Transform.scala +30 -6
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +7 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +28 -2
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +23 -5
- package/project/.sdk/tm/ts/src/Response.ts +14 -1
- package/project/.sdk/tm/ts/src/utility/MakeUrlUtility.ts +7 -0
- package/project/.sdk/tm/ts/src/utility/PrepareHeadersUtility.ts +16 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +3 -1
- package/project/.sdk/tm/ts/src/utility/TransformRequestUtility.ts +16 -3
- package/project/.sdk/tm/ts/test/definition-runner.ts +75 -13
- package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +25 -0
- package/project/.sdk/tm/zig/core/utility.zig +80 -20
- package/project/sdkgen-package.json +1 -1
- package/src/helpers/definitionPlan.ts +49 -7
|
@@ -80,6 +80,15 @@ object PrepareQuery {
|
|
|
80
80
|
}
|
|
81
81
|
case _ =>
|
|
82
82
|
}
|
|
83
|
+
// A header parameter travels in the headers, which prepareHeaders fills.
|
|
84
|
+
Struct.getpath(point, java.util.List.of("args", "header")) match {
|
|
85
|
+
case l: JList[_] =>
|
|
86
|
+
val hit = l.iterator()
|
|
87
|
+
while (hit.hasNext) {
|
|
88
|
+
Struct.getprop(hit.next(), "name") match { case s: String => params.add(s); case _ => }
|
|
89
|
+
}
|
|
90
|
+
case _ =>
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// A query parameter travels under the name the definition gives it, its
|
|
@@ -124,9 +133,39 @@ object PrepareHeaders {
|
|
|
124
133
|
def prepareHeaders(ctx: Context): JMap[String, Object] = {
|
|
125
134
|
val options = ctx.client.optionsMap()
|
|
126
135
|
val headers = Struct.getprop(options, "headers")
|
|
127
|
-
if (headers == null)
|
|
128
|
-
val out =
|
|
129
|
-
|
|
136
|
+
val cloned = if (headers == null) null else Helpers.toMapAny(Struct.clone(headers))
|
|
137
|
+
val out: JMap[String, Object] = if (cloned != null) cloned else new LinkedHashMap[String, Object]()
|
|
138
|
+
|
|
139
|
+
// A header parameter travels as a header, under the name the definition
|
|
140
|
+
// gives it, and only from this call's own arguments. It replaces a default
|
|
141
|
+
// of the same name, whatever its case.
|
|
142
|
+
if (ctx.point != null) {
|
|
143
|
+
Struct.getpath(ctx.point, java.util.List.of("args", "header")) match {
|
|
144
|
+
case l: JList[_] =>
|
|
145
|
+
val hit = l.iterator()
|
|
146
|
+
while (hit.hasNext) {
|
|
147
|
+
val hd = hit.next()
|
|
148
|
+
Struct.getprop(hd, "name") match {
|
|
149
|
+
case name: String if name.nonEmpty =>
|
|
150
|
+
val wire = Struct.getprop(hd, "orig") match {
|
|
151
|
+
case o: String if o.nonEmpty => o
|
|
152
|
+
case _ => name
|
|
153
|
+
}
|
|
154
|
+
var v: Object = if (ctx.reqmatch == null) null else Struct.getprop(ctx.reqmatch, name, null)
|
|
155
|
+
if (v == null && ctx.reqdata != null) v = Struct.getprop(ctx.reqdata, name, null)
|
|
156
|
+
if (v != null) {
|
|
157
|
+
val key = wire.toLowerCase(java.util.Locale.ROOT)
|
|
158
|
+
out.keySet().removeIf(k => k != null && k.toLowerCase(java.util.Locale.ROOT) == key)
|
|
159
|
+
out.put(key, Struct.stringify(v))
|
|
160
|
+
}
|
|
161
|
+
case _ =>
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
case _ =>
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
out
|
|
130
169
|
}
|
|
131
170
|
}
|
|
132
171
|
|
|
@@ -8,28 +8,52 @@ object TransformRequest {
|
|
|
8
8
|
def transformRequest(ctx: Context): Object = {
|
|
9
9
|
if (ctx.spec != null) ctx.spec.step = "reqform"
|
|
10
10
|
|
|
11
|
+
val reqdata = omit(ctx.reqdata, headerArgNames(ctx))
|
|
12
|
+
|
|
11
13
|
val transform = Helpers.toMapAny(Struct.getprop(ctx.point, "transform"))
|
|
12
|
-
if (transform == null) return stripAction(
|
|
14
|
+
if (transform == null) return stripAction(reqdata)
|
|
13
15
|
|
|
14
16
|
val reqform = Struct.getprop(transform, "req", null)
|
|
15
|
-
if (reqform == null) return stripAction(
|
|
17
|
+
if (reqform == null) return stripAction(reqdata)
|
|
16
18
|
|
|
17
19
|
val data = new LinkedHashMap[String, Object]()
|
|
18
|
-
data.put("reqdata",
|
|
20
|
+
data.put("reqdata", reqdata)
|
|
19
21
|
|
|
20
22
|
stripAction(Struct.transform(data, reqform))
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// `$action` selects the point (see MakePoint); it is never an API field, so
|
|
24
26
|
// the body is a copy without it. The caller's map is left untouched.
|
|
25
|
-
private def stripAction(reqdata: Object): Object =
|
|
27
|
+
private def stripAction(reqdata: Object): Object = omit(reqdata, Seq("$action"))
|
|
28
|
+
|
|
29
|
+
// A header argument travels as a header, which PrepareHeaders sends, so the
|
|
30
|
+
// body is built from the request data without it.
|
|
31
|
+
private def headerArgNames(ctx: Context): Seq[String] = {
|
|
32
|
+
if (ctx.point == null) return Seq.empty
|
|
33
|
+
Struct.getpath(ctx.point, java.util.List.of("args", "header")) match {
|
|
34
|
+
case l: java.util.List[_] =>
|
|
35
|
+
val names = scala.collection.mutable.ArrayBuffer[String]()
|
|
36
|
+
val hit = l.iterator()
|
|
37
|
+
while (hit.hasNext) {
|
|
38
|
+
val hd = hit.next()
|
|
39
|
+
Struct.getprop(hd, "name") match {
|
|
40
|
+
case name: String if name.nonEmpty => names += name
|
|
41
|
+
case _ =>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
names.toSeq
|
|
45
|
+
case _ => Seq.empty
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private def omit(reqdata: Object, names: Seq[String]): Object = {
|
|
26
50
|
reqdata match {
|
|
27
|
-
case src: JMap[_, _] if src.containsKey(
|
|
51
|
+
case src: JMap[_, _] if names.exists(n => src.containsKey(n)) =>
|
|
28
52
|
val body = new LinkedHashMap[String, Object]()
|
|
29
53
|
val it = src.asInstanceOf[JMap[String, Object]].entrySet().iterator()
|
|
30
54
|
while (it.hasNext) {
|
|
31
55
|
val e = it.next()
|
|
32
|
-
if (
|
|
56
|
+
if (!names.contains(e.getKey)) body.put(e.getKey, e.getValue)
|
|
33
57
|
}
|
|
34
58
|
body
|
|
35
59
|
case _ => reqdata
|
|
@@ -208,6 +208,13 @@ func makeUrlUtil(_ ctx: Context) throws -> String {
|
|
|
208
208
|
var url = join(jtp(spec.base, spec.prefix, spec.path, spec.suffix), "/", true)
|
|
209
209
|
let resmatch = VMap()
|
|
210
210
|
|
|
211
|
+
// A route the definition ends with a slash keeps it: a server such as a
|
|
212
|
+
// Django REST one redirects or refuses the route without it.
|
|
213
|
+
if let orig = gp(ctx.point, "orig").asString, orig.hasSuffix("/"), spec.suffix.isEmpty,
|
|
214
|
+
!url.hasSuffix("/") {
|
|
215
|
+
url += "/"
|
|
216
|
+
}
|
|
217
|
+
|
|
211
218
|
for item in items(.map(spec.params)) {
|
|
212
219
|
let key = item[0].asString ?? ""
|
|
213
220
|
let val = item[1]
|
|
@@ -39,8 +39,28 @@ func preparePathUtil(_ ctx: Context) -> String {
|
|
|
39
39
|
func prepareHeadersUtil(_ ctx: Context) -> VMap {
|
|
40
40
|
let options = ctx.client!.optionsMap()
|
|
41
41
|
let headers = gp(options, "headers")
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
let out = isNil(headers) ? VMap() : (clone(headers).asMap ?? VMap())
|
|
43
|
+
|
|
44
|
+
// A header parameter travels as a header, under the name the definition
|
|
45
|
+
// gives it, and only from this call's own arguments. It replaces a default
|
|
46
|
+
// of the same name, whatever its case.
|
|
47
|
+
if let ahl = gpath(ctx.point, "args", "header").asList {
|
|
48
|
+
for hd in ahl.items {
|
|
49
|
+
guard let name = gp(hd, "name").asString, !name.isEmpty else { continue }
|
|
50
|
+
let orig = gp(hd, "orig").asString ?? ""
|
|
51
|
+
let wire = orig.isEmpty ? name : orig
|
|
52
|
+
var val = gp(ctx.reqmatch, name)
|
|
53
|
+
if isNil(val) { val = gp(ctx.reqdata, name) }
|
|
54
|
+
if !isNil(val) {
|
|
55
|
+
let key = wire.lowercased()
|
|
56
|
+
for k in out.entries.keys where k.lowercased() == key {
|
|
57
|
+
_ = out.entries.removeValue(forKey: k)
|
|
58
|
+
}
|
|
59
|
+
out.entries[key] = .string(stringify(val))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
func prepareParamsUtil(_ ctx: Context) -> VMap {
|
|
@@ -73,6 +93,12 @@ func prepareQueryUtil(_ ctx: Context) -> VMap {
|
|
|
73
93
|
paramnames.append(gp(pd, "name"))
|
|
74
94
|
}
|
|
75
95
|
}
|
|
96
|
+
// A header parameter travels in the headers, which prepareHeaders fills.
|
|
97
|
+
if let ahl = gpath(ctx.point, "args", "header").asList {
|
|
98
|
+
for hd in ahl.items {
|
|
99
|
+
paramnames.append(gp(hd, "name"))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
76
102
|
|
|
77
103
|
// A query parameter travels under the name the definition gives it, its
|
|
78
104
|
// orig, which the model may have renamed for the caller.
|
|
@@ -57,9 +57,25 @@ func resultHeadersUtil(_ ctx: Context) -> Result {
|
|
|
57
57
|
// `$action` selects the point (see makePointUtil); it is never an API field,
|
|
58
58
|
// so the body is a copy without it. The caller's map is left untouched.
|
|
59
59
|
private func stripAction(_ reqdata: Value) -> Value {
|
|
60
|
-
|
|
60
|
+
return omitKeys(reqdata, ["$action"])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A header argument travels as a header, which prepareHeadersUtil sends, so
|
|
64
|
+
// the body is built from the request data without it.
|
|
65
|
+
private func headerArgNames(_ ctx: Context) -> [String] {
|
|
66
|
+
guard let ahl = gpath(ctx.point, "args", "header").asList else { return [] }
|
|
67
|
+
return ahl.items.compactMap { hd in
|
|
68
|
+
guard let name = gp(hd, "name").asString, !name.isEmpty else { return nil }
|
|
69
|
+
return name
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func omitKeys(_ reqdata: Value, _ names: [String]) -> Value {
|
|
74
|
+
guard let src = reqdata.asMap, names.contains(where: { src.entries[$0] != nil }) else {
|
|
75
|
+
return reqdata
|
|
76
|
+
}
|
|
61
77
|
let body = VMap()
|
|
62
|
-
for (key, val) in src.entries where
|
|
78
|
+
for (key, val) in src.entries where !names.contains(key) {
|
|
63
79
|
body.entries[key] = val
|
|
64
80
|
}
|
|
65
81
|
return .map(body)
|
|
@@ -68,11 +84,13 @@ private func stripAction(_ reqdata: Value) -> Value {
|
|
|
68
84
|
func transformRequestUtil(_ ctx: Context) -> Value {
|
|
69
85
|
if let sp = ctx.spec { sp.step = "reqform" }
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
let reqdata = omitKeys(.map(ctx.reqdata), headerArgNames(ctx))
|
|
88
|
+
|
|
89
|
+
guard let tfm = gp(ctx.point, "transform").asMap else { return stripAction(reqdata) }
|
|
72
90
|
let reqform = gp(tfm, "req")
|
|
73
|
-
if isNil(reqform) { return stripAction(
|
|
91
|
+
if isNil(reqform) { return stripAction(reqdata) }
|
|
74
92
|
|
|
75
|
-
return stripAction(transform(.map(vm(("reqdata",
|
|
93
|
+
return stripAction(transform(.map(vm(("reqdata", reqdata))), reqform))
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
func transformResponseUtil(_ ctx: Context) -> Value {
|
|
@@ -14,13 +14,26 @@ class Response {
|
|
|
14
14
|
this.status = getprop(resmap, 'status', -1)
|
|
15
15
|
this.statusText = getprop(resmap, 'statusText', '')
|
|
16
16
|
this.headers = getprop(resmap, 'headers')
|
|
17
|
-
this.json =
|
|
17
|
+
this.json = readJson(resmap)
|
|
18
18
|
this.body = getprop(resmap, 'body')
|
|
19
19
|
this.err = getprop(resmap, 'err')
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
// An empty body, such as the one an accepted delete answers with, is no
|
|
25
|
+
// body: parsing it as JSON would throw after the call has succeeded.
|
|
26
|
+
function readJson(resmap: Record<string, any>): Function {
|
|
27
|
+
if ('function' === typeof resmap.text) {
|
|
28
|
+
return async () => {
|
|
29
|
+
const text = await resmap.text()
|
|
30
|
+
return '' === text.trim() ? undefined : JSON.parse(text)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return resmap.json ? resmap.json.bind(resmap) : async () => undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
export {
|
|
25
38
|
Response,
|
|
26
39
|
}
|
|
@@ -26,6 +26,13 @@ function makeUrl(ctx: Context): Error | string {
|
|
|
26
26
|
let url = join([spec.base, spec.prefix, spec.path, spec.suffix], '/', true)
|
|
27
27
|
let resmatch: Record<string, any> = {}
|
|
28
28
|
|
|
29
|
+
// A route the definition ends with a slash keeps it: a server such as a
|
|
30
|
+
// Django REST one redirects or refuses the route without it.
|
|
31
|
+
const orig = ctx.point?.orig
|
|
32
|
+
if ('string' === typeof orig && orig.endsWith('/') && !spec.suffix && !url.endsWith('/')) {
|
|
33
|
+
url += '/'
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
const params = spec.params
|
|
30
37
|
|
|
31
38
|
for (let [key, val] of items(params)) {
|
|
@@ -6,6 +6,7 @@ function prepareHeaders(ctx: Context) {
|
|
|
6
6
|
const struct = ctx.utility.struct
|
|
7
7
|
const clone = struct.clone
|
|
8
8
|
const getprop = struct.getprop
|
|
9
|
+
const stringify = struct.stringify
|
|
9
10
|
|
|
10
11
|
const client = ctx.client
|
|
11
12
|
|
|
@@ -13,6 +14,21 @@ function prepareHeaders(ctx: Context) {
|
|
|
13
14
|
|
|
14
15
|
let out = clone(getprop(options, 'headers', {}))
|
|
15
16
|
|
|
17
|
+
// A header parameter travels as a header, under the name the definition
|
|
18
|
+
// gives it, and only from this call's own arguments. It replaces a default
|
|
19
|
+
// of the same name, whatever its case.
|
|
20
|
+
for (const h of (ctx.point?.args?.header || [])) {
|
|
21
|
+
if ('string' !== typeof h?.name || '' === h.name) continue
|
|
22
|
+
const val = getprop(ctx.reqmatch, h.name) ?? getprop(ctx.reqdata, h.name)
|
|
23
|
+
if (null != val) {
|
|
24
|
+
const wire = String(h.orig || h.name).toLowerCase()
|
|
25
|
+
for (const key of Object.keys(out)) {
|
|
26
|
+
if (wire === key.toLowerCase()) delete out[key]
|
|
27
|
+
}
|
|
28
|
+
out[wire] = stringify(val)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
return out
|
|
17
33
|
}
|
|
18
34
|
|
|
@@ -16,8 +16,10 @@ function prepareQuery(ctx: Context) {
|
|
|
16
16
|
|
|
17
17
|
// A path parameter travels in the path. The generated config lists them as
|
|
18
18
|
// args.params, which prepareParams reads; params is the older list of names.
|
|
19
|
+
// A header parameter travels in the headers, which prepareHeaders fills.
|
|
19
20
|
const inpath: string[] = params.concat(
|
|
20
|
-
(point.args?.params || []).map((p: any) => p?.name)
|
|
21
|
+
(point.args?.params || []).map((p: any) => p?.name),
|
|
22
|
+
(point.args?.header || []).map((h: any) => h?.name))
|
|
21
23
|
|
|
22
24
|
// A query parameter travels under the name the definition gives it, its
|
|
23
25
|
// orig, which the model may have renamed for the caller.
|
|
@@ -15,7 +15,7 @@ function transformRequest(ctx: Context) {
|
|
|
15
15
|
try {
|
|
16
16
|
const reqform = point.transform.req
|
|
17
17
|
const reqdata = isfunc(reqform) ? reqform(ctx) : transform({
|
|
18
|
-
reqdata: ctx.reqdata
|
|
18
|
+
reqdata: omit(ctx.reqdata, headerArgNames(ctx))
|
|
19
19
|
}, reqform)
|
|
20
20
|
|
|
21
21
|
return stripAction(reqdata)
|
|
@@ -29,17 +29,30 @@ function transformRequest(ctx: Context) {
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
function stripAction(reqdata: any) {
|
|
32
|
+
return omit(reqdata, ['$action'])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// A header argument travels as a header, which prepareHeaders sends, so the
|
|
37
|
+
// body is built from the request data without it.
|
|
38
|
+
function headerArgNames(ctx: Context): string[] {
|
|
39
|
+
return (ctx.point?.args?.header || []).map((h: any) => h?.name)
|
|
40
|
+
.filter((name: any) => 'string' === typeof name && '' !== name)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function omit(reqdata: any, names: string[]) {
|
|
32
45
|
if (null == reqdata || 'object' !== typeof reqdata || Array.isArray(reqdata)) {
|
|
33
46
|
return reqdata
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
if (!Object.prototype.hasOwnProperty.call(reqdata,
|
|
49
|
+
if (!names.some((name) => Object.prototype.hasOwnProperty.call(reqdata, name))) {
|
|
37
50
|
return reqdata
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
const body: Record<string, any> = {}
|
|
41
54
|
for (const key of Object.keys(reqdata)) {
|
|
42
|
-
if (
|
|
55
|
+
if (names.includes(key)) {
|
|
43
56
|
continue
|
|
44
57
|
}
|
|
45
58
|
if ('__proto__' === key) {
|
|
@@ -16,6 +16,7 @@ type DefinitionPoint = {
|
|
|
16
16
|
action?: string
|
|
17
17
|
args: { name: string, wire: string, value: any }[]
|
|
18
18
|
select: Record<string, any>
|
|
19
|
+
headers?: { name: string, wire: string, value: any }[]
|
|
19
20
|
query: string[]
|
|
20
21
|
auth: Credential[][] | null
|
|
21
22
|
status: number
|
|
@@ -49,6 +50,7 @@ async function runDefinitionPoint(SDK: any, point: DefinitionPoint): Promise<voi
|
|
|
49
50
|
|
|
50
51
|
const input: any = { ...point.select }
|
|
51
52
|
for (const arg of point.args) input[arg.name] = arg.value
|
|
53
|
+
for (const h of point.headers || []) input[h.name] = h.value
|
|
52
54
|
if (null != point.action) input.$action = point.action
|
|
53
55
|
|
|
54
56
|
let result: any
|
|
@@ -70,7 +72,9 @@ async function runDefinitionPoint(SDK: any, point: DefinitionPoint): Promise<voi
|
|
|
70
72
|
const arg = point.args.find((a) => a.wire === name)
|
|
71
73
|
return null == arg ? '{' + name + '}' : encodeURIComponent(String(arg.value))
|
|
72
74
|
})
|
|
73
|
-
|
|
75
|
+
// Read as the request was, so a backslash in the definition's path, as in
|
|
76
|
+
// GitLab's `Packages\(\)`, is the slash the URL parser makes it.
|
|
77
|
+
assert.equal(url.pathname, new URL(BASE + route).pathname, 'route')
|
|
74
78
|
|
|
75
79
|
// Only what the definition declares, so never a path parameter again.
|
|
76
80
|
const credentialQuery = (point.auth || []).flat()
|
|
@@ -87,6 +91,18 @@ async function runDefinitionPoint(SDK: any, point: DefinitionPoint): Promise<voi
|
|
|
87
91
|
'credential not sent as the definition declares it: ' + JSON.stringify(point.auth))
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// A header parameter goes out as a header. The credential check above owns
|
|
95
|
+
// any header the security scheme names, and the SDK sets the content type
|
|
96
|
+
// from the body it sends.
|
|
97
|
+
const credentialHeaders = (point.auth || []).flat()
|
|
98
|
+
.filter((c) => 'header' === c.in).map((c) => c.name.toLowerCase())
|
|
99
|
+
for (const h of point.headers || []) {
|
|
100
|
+
const wire = h.wire.toLowerCase()
|
|
101
|
+
if (credentialHeaders.includes(wire) || 'content-type' === wire) continue
|
|
102
|
+
assert.equal(new Headers(init.headers).get(wire), String(h.value),
|
|
103
|
+
'header parameter not sent as a header: ' + h.wire)
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
if (null != error) {
|
|
91
107
|
throw error
|
|
92
108
|
}
|
|
@@ -105,7 +121,7 @@ async function runDefinitionPoint(SDK: any, point: DefinitionPoint): Promise<voi
|
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
123
|
else if ('load' === point.op || 'create' === point.op || 'update' === point.op) {
|
|
108
|
-
const record = recordOf(point.sample, point.idField)
|
|
124
|
+
const record = recordOf(point.sample, point.idField, point.entity)
|
|
109
125
|
if (null != record) {
|
|
110
126
|
assert.equal(result?.data?.()?.[point.idField], record[point.idField],
|
|
111
127
|
'the entity does not hold the record the definition example returns')
|
|
@@ -145,33 +161,79 @@ function placed(cred: Credential, headers: Headers, url: URL): boolean {
|
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
164
|
+
// What an envelope may hold beside what it carries, compared without case,
|
|
165
|
+
// `_` or `-`: status, paging and the page's own metadata, each by its whole
|
|
166
|
+
// name, so a record's homepage or preview is data of its own.
|
|
167
|
+
const ENVELOPE_KEYS = new Set([
|
|
168
|
+
'success', 'status', 'ok', 'message', 'code', 'error', 'errorcode', 'errormessage',
|
|
169
|
+
'requestid', 'timestamp', 'took', 'version', 'apiversion', 'object', 'url',
|
|
170
|
+
'count', 'total', 'totalcount', 'totalhits', 'totalitems', 'totalpages', 'totalresults',
|
|
171
|
+
'totalrecords', 'totalrowcount', 'totalentries', 'itemcount', 'resultcount', 'rowcount',
|
|
172
|
+
'page', 'pages', 'pagecount', 'pagenumber', 'pageindex', 'pagesize', 'perpage',
|
|
173
|
+
'currentpage', 'lastpage', 'limit', 'offset', 'cursor', 'nextcursor', 'prevcursor',
|
|
174
|
+
'previouscursor', 'next', 'nextpage', 'nexturl', 'nextlink', 'nextpagetoken', 'nexttoken',
|
|
175
|
+
'pagetoken', 'continuationtoken', 'prev', 'previous', 'prevpage', 'previouspage',
|
|
176
|
+
'prevurl', 'previousurl', 'prevlink', 'hasmore', 'hasnext', 'hasnextpage', 'hasprevious',
|
|
177
|
+
'haspreviouspage', 'more', 'meta', 'metadata', 'pagination', 'paging', 'pageinfo', 'links',
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
function envelopeKey(key: string): boolean {
|
|
182
|
+
return ENVELOPE_KEYS.has(squash(key))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
function ownData(sample: any): boolean {
|
|
187
|
+
return Object.entries(sample).some(([key, value]) =>
|
|
188
|
+
null != value && 'object' !== typeof value && !envelopeKey(key))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
function isRecord(value: any): boolean {
|
|
193
|
+
return null != value && 'object' === typeof value && !Array.isArray(value)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// The records a list response holds: the body itself, or the one non-empty
|
|
198
|
+
// list of objects in a page. Anything else, such as a record that happens to
|
|
199
|
+
// hold a list, proves nothing.
|
|
150
200
|
function recordsOf(sample: any): any[] | null {
|
|
151
201
|
if (Array.isArray(sample)) {
|
|
152
202
|
return sample
|
|
153
203
|
}
|
|
154
|
-
if (
|
|
204
|
+
if (!isRecord(sample) || ownData(sample)) {
|
|
155
205
|
return null
|
|
156
206
|
}
|
|
157
207
|
const lists = Object.values(sample).filter((v: any) => Array.isArray(v) && 0 < v.length &&
|
|
158
|
-
v.every(
|
|
208
|
+
v.every(isRecord))
|
|
159
209
|
return 1 === lists.length ? lists[0] as any[] : null
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
|
|
163
|
-
// The record a single-item response returns: the body, or
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
213
|
+
// The record a single-item response returns: the body, or the one object
|
|
214
|
+
// with the identity field that an envelope carries, under the entity's name
|
|
215
|
+
// or beside envelope keys alone. Otherwise the body is the record itself,
|
|
216
|
+
// such as GitHub's check suite preferences beside their repository.
|
|
217
|
+
function recordOf(sample: any, idField: string, entity?: string): any {
|
|
218
|
+
if (!isRecord(sample)) {
|
|
167
219
|
return null
|
|
168
220
|
}
|
|
169
221
|
if (null != sample[idField]) {
|
|
170
222
|
return sample
|
|
171
223
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
224
|
+
const keys = Object.keys(sample)
|
|
225
|
+
const inner = keys.filter((key) => isRecord(sample[key]) && null != sample[key][idField])
|
|
226
|
+
if (1 !== inner.length) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
const named = null != entity && squash(inner[0]) === squash(entity) && !ownData(sample)
|
|
230
|
+
const alone = keys.every((key) => key === inner[0] || envelopeKey(key))
|
|
231
|
+
return named || alone ? sample[inner[0]] : null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
function squash(name: string): string {
|
|
236
|
+
return name.toLowerCase().replace(/[_-]/g, '')
|
|
175
237
|
}
|
|
176
238
|
|
|
177
239
|
|
|
@@ -259,6 +259,31 @@ describe('PrimaryUtility', async () => {
|
|
|
259
259
|
})
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
// An accepted delete answers 202 with an empty body, which is no body.
|
|
263
|
+
test('resbody-empty', async () => {
|
|
264
|
+
const read = async (res: any) => {
|
|
265
|
+
const reqClient = new (SDK as any)({
|
|
266
|
+
base: 'http://localhost:8080',
|
|
267
|
+
system: { fetch: async () => res },
|
|
268
|
+
})
|
|
269
|
+
const reqUtility = reqClient.utility()
|
|
270
|
+
const ctx = reqUtility.makeContext({
|
|
271
|
+
opname: 'remove',
|
|
272
|
+
spec: {
|
|
273
|
+
alias: {}, base: 'http://localhost/', headers: {}, method: 'DELETE',
|
|
274
|
+
params: {}, path: '/p0', prefix: '', query: {}, suffix: '',
|
|
275
|
+
},
|
|
276
|
+
}, reqClient._rootctx)
|
|
277
|
+
ctx.client = reqClient
|
|
278
|
+
await reqUtility.makeRequest(ctx)
|
|
279
|
+
return reqUtility.resultBody(ctx)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
equal((await read(new Response('', { status: 202 }))).body, undefined)
|
|
283
|
+
deepStrictEqual((await read(new Response('{"a":1}', { status: 200 }))).body, { a: 1 })
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
|
|
262
287
|
test('request-basic', async () => {
|
|
263
288
|
const mockFetch = async (url: string, init: any) => ({
|
|
264
289
|
status: 200,
|