@voxgig/sdkgen 4.31.0 → 4.32.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/action/package.d.ts +2 -1
- package/dist/action/package.js +8 -1
- package/dist/action/package.js.map +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/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/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/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/zig/core/utility.zig +80 -20
- package/project/sdkgen-package.json +1 -1
- package/src/action/package.ts +10 -1
- package/src/helpers/definitionPlan.ts +49 -7
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -764,6 +764,15 @@ pub fn make_url_util(ctx: *Context) E![]const u8 {
|
|
|
764
764
|
h.vstr(spec.suffix),
|
|
765
765
|
}), "/", true) catch "";
|
|
766
766
|
|
|
767
|
+
// A route the definition ends with a slash keeps it: a server such as a
|
|
768
|
+
// Django REST one redirects or refuses the route without it.
|
|
769
|
+
const orig = h.getp(ctx.point, "orig");
|
|
770
|
+
if (orig == .string and std.mem.endsWith(u8, orig.string, "/") and spec.suffix.len == 0 and
|
|
771
|
+
!std.mem.endsWith(u8, url, "/"))
|
|
772
|
+
{
|
|
773
|
+
url = std.mem.concat(h.A(), u8, &.{ url, "/" }) catch url;
|
|
774
|
+
}
|
|
775
|
+
|
|
767
776
|
const resmatch = h.omap();
|
|
768
777
|
|
|
769
778
|
if (spec.params == .object) {
|
|
@@ -1043,12 +1052,37 @@ pub fn prepare_headers_util(ctx: *Context) Value {
|
|
|
1043
1052
|
const options: Value = if (ctx.client) |client| client.options_map() else ctx.options;
|
|
1044
1053
|
|
|
1045
1054
|
const headers = h.getp(options, "headers");
|
|
1046
|
-
if (h.is_noval(headers))
|
|
1047
|
-
|
|
1048
|
-
return switch (h.clone(headers)) {
|
|
1055
|
+
const out: Value = if (h.is_noval(headers)) h.omap() else switch (h.clone(headers)) {
|
|
1049
1056
|
.object => h.clone(headers),
|
|
1050
1057
|
else => h.omap(),
|
|
1051
1058
|
};
|
|
1059
|
+
|
|
1060
|
+
// A header parameter travels as a header, under the name the definition
|
|
1061
|
+
// gives it, and only from this call's own arguments. It replaces a default
|
|
1062
|
+
// of the same name, whatever its case.
|
|
1063
|
+
const aheader: Value = h.getpath(&.{ "args", "header" }, ctx.point);
|
|
1064
|
+
if (aheader == .array) {
|
|
1065
|
+
for (aheader.array.data.items) |hd| {
|
|
1066
|
+
const name = h.getp(hd, "name");
|
|
1067
|
+
if (name != .string or name.string.len == 0) continue;
|
|
1068
|
+
const orig = h.getp(hd, "orig");
|
|
1069
|
+
const wire: []const u8 = if (orig == .string and orig.string.len != 0) orig.string else name.string;
|
|
1070
|
+
var val = h.getp(ctx.reqmatch, name.string);
|
|
1071
|
+
if (h.is_noval(val)) val = h.getp(ctx.reqdata, name.string);
|
|
1072
|
+
if (h.is_noval(val)) continue;
|
|
1073
|
+
const key = std.ascii.allocLowerString(h.A(), wire) catch wire;
|
|
1074
|
+
while (true) {
|
|
1075
|
+
var kit = out.object.iterator();
|
|
1076
|
+
const same: ?[]const u8 = while (kit.next()) |kv| {
|
|
1077
|
+
if (std.ascii.eqlIgnoreCase(kv.key_ptr.*, key)) break kv.key_ptr.*;
|
|
1078
|
+
} else null;
|
|
1079
|
+
_ = out.object.fetchOrderedRemove(same orelse break);
|
|
1080
|
+
}
|
|
1081
|
+
h.setp(out, key, h.vstr(h.stringify(val)));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return out;
|
|
1052
1086
|
}
|
|
1053
1087
|
|
|
1054
1088
|
pub fn prepare_body_util(ctx: *Context) Value {
|
|
@@ -1094,6 +1128,16 @@ pub fn prepare_path_util(ctx: *Context) []const u8 {
|
|
|
1094
1128
|
return vs.join(h.A(), parts, "/", true) catch "";
|
|
1095
1129
|
}
|
|
1096
1130
|
|
|
1131
|
+
// Whether a list of argument definitions names this key.
|
|
1132
|
+
fn names_key(defs: Value, key: []const u8) bool {
|
|
1133
|
+
if (defs != .array) return false;
|
|
1134
|
+
for (defs.array.data.items) |d| {
|
|
1135
|
+
const name = h.getp(d, "name");
|
|
1136
|
+
if (name == .string and std.mem.eql(u8, name.string, key)) return true;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1097
1141
|
pub fn prepare_query_util(ctx: *Context) Value {
|
|
1098
1142
|
const point = ctx.point;
|
|
1099
1143
|
const reqmatch: Value = switch (ctx.reqmatch) {
|
|
@@ -1107,6 +1151,8 @@ pub fn prepare_query_util(ctx: *Context) Value {
|
|
|
1107
1151
|
// A path parameter travels in the path. The generated config lists them
|
|
1108
1152
|
// as args.params, which prepare_params reads; params is the older list.
|
|
1109
1153
|
const aparams: Value = h.getpath(&.{ "args", "params" }, point);
|
|
1154
|
+
// A header parameter travels in the headers, which prepare_headers fills.
|
|
1155
|
+
const aheader: Value = h.getpath(&.{ "args", "header" }, point);
|
|
1110
1156
|
// A query parameter travels under the name the definition gives it, its
|
|
1111
1157
|
// orig, which the model may have renamed for the caller.
|
|
1112
1158
|
const aquery: Value = h.getpath(&.{ "args", "query" }, point);
|
|
@@ -1126,15 +1172,7 @@ pub fn prepare_query_util(ctx: *Context) Value {
|
|
|
1126
1172
|
}
|
|
1127
1173
|
}
|
|
1128
1174
|
}
|
|
1129
|
-
if (!contained
|
|
1130
|
-
for (aparams.array.data.items) |pd| {
|
|
1131
|
-
const name = h.getp(pd, "name");
|
|
1132
|
-
if (name == .string and std.mem.eql(u8, name.string, key)) {
|
|
1133
|
-
contained = true;
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1175
|
+
if (!contained) contained = names_key(aparams, key) or names_key(aheader, key);
|
|
1138
1176
|
var wire: []const u8 = key;
|
|
1139
1177
|
if (aquery == .array) {
|
|
1140
1178
|
for (aquery.array.data.items) |qd| {
|
|
@@ -1406,13 +1444,33 @@ pub fn result_body_util(ctx: *Context) ?*SdkResult {
|
|
|
1406
1444
|
// `$action` selects the point (see make_point_util); it is never an API
|
|
1407
1445
|
// field, so the body is a copy without it. The caller's map is left untouched.
|
|
1408
1446
|
fn strip_action(reqdata: Value) Value {
|
|
1447
|
+
return omit_keys(reqdata, h.vnull(), true);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// A header argument travels as a header, which prepare_headers_util sends, so
|
|
1451
|
+
// the body is built from the request data without it.
|
|
1452
|
+
fn dropped(key: []const u8, point: Value, action: bool) bool {
|
|
1453
|
+
if (action) return std.mem.eql(u8, key, "$action");
|
|
1454
|
+
const aheader: Value = h.getpath(&.{ "args", "header" }, point);
|
|
1455
|
+
if (aheader != .array) return false;
|
|
1456
|
+
for (aheader.array.data.items) |hd| {
|
|
1457
|
+
const name = h.getp(hd, "name");
|
|
1458
|
+
if (name == .string and std.mem.eql(u8, name.string, key)) return true;
|
|
1459
|
+
}
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
fn omit_keys(reqdata: Value, point: Value, action: bool) Value {
|
|
1409
1464
|
if (reqdata != .object) return reqdata;
|
|
1410
|
-
|
|
1411
|
-
const body = h.omap();
|
|
1465
|
+
var found = false;
|
|
1412
1466
|
var it = reqdata.object.iterator();
|
|
1413
|
-
while (it.next()) |kv|
|
|
1467
|
+
while (it.next()) |kv| found = found or dropped(kv.key_ptr.*, point, action);
|
|
1468
|
+
if (!found) return reqdata;
|
|
1469
|
+
const body = h.omap();
|
|
1470
|
+
var bit = reqdata.object.iterator();
|
|
1471
|
+
while (bit.next()) |kv| {
|
|
1414
1472
|
const key = kv.key_ptr.*;
|
|
1415
|
-
if (!
|
|
1473
|
+
if (!dropped(key, point, action)) h.setp(body, key, kv.value_ptr.*);
|
|
1416
1474
|
}
|
|
1417
1475
|
return body;
|
|
1418
1476
|
}
|
|
@@ -1423,16 +1481,18 @@ pub fn transform_request_util(ctx: *Context) Value {
|
|
|
1423
1481
|
|
|
1424
1482
|
if (spec) |sp| sp.step = "reqform";
|
|
1425
1483
|
|
|
1484
|
+
const reqdata = omit_keys(ctx.reqdata, point, false);
|
|
1485
|
+
|
|
1426
1486
|
const transform = h.to_map(h.getp(point, "transform"));
|
|
1427
|
-
if (h.is_noval(transform)) return strip_action(
|
|
1487
|
+
if (h.is_noval(transform)) return strip_action(reqdata);
|
|
1428
1488
|
|
|
1429
1489
|
const reqform = h.getp(transform, "req");
|
|
1430
|
-
if (h.is_noval(reqform)) return strip_action(
|
|
1490
|
+
if (h.is_noval(reqform)) return strip_action(reqdata);
|
|
1431
1491
|
|
|
1432
|
-
const store = h.jo(&.{.{ "reqdata",
|
|
1492
|
+
const store = h.jo(&.{.{ "reqdata", reqdata }});
|
|
1433
1493
|
// transform now reports collected injection errors beside the value; .out
|
|
1434
1494
|
// is what it used to return on its own, errors or not.
|
|
1435
|
-
const tres = vs.transform(h.A(), store, reqform) catch return strip_action(
|
|
1495
|
+
const tres = vs.transform(h.A(), store, reqform) catch return strip_action(reqdata);
|
|
1436
1496
|
return strip_action(tres.out);
|
|
1437
1497
|
}
|
|
1438
1498
|
|
package/src/action/package.ts
CHANGED
|
@@ -737,7 +737,7 @@ async function npmFetch(pkgname: string, actx: ActionContext) {
|
|
|
737
737
|
try {
|
|
738
738
|
const out = await run(
|
|
739
739
|
'win32' === process.platform ? 'npm.cmd' : 'npm',
|
|
740
|
-
|
|
740
|
+
npmFetchArgs(pkgname),
|
|
741
741
|
{ cwd, maxBuffer: 64 * 1024 * 1024 })
|
|
742
742
|
|
|
743
743
|
actx.log.debug({
|
|
@@ -755,6 +755,14 @@ async function npmFetch(pkgname: string, actx: ActionContext) {
|
|
|
755
755
|
}
|
|
756
756
|
|
|
757
757
|
|
|
758
|
+
// The saved range is a tilde, as the scaffold writes it: a caret would let
|
|
759
|
+
// the next minor float in, and a scaffold contract can change in a minor.
|
|
760
|
+
// An .npmrc `save-exact` outranks the prefix, so it is switched off here.
|
|
761
|
+
function npmFetchArgs(pkgname: string): string[] {
|
|
762
|
+
return ['install', '--save-dev', '--save-exact=false', '--save-prefix=~', pkgname + '@latest']
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
|
|
758
766
|
function validateFetched(
|
|
759
767
|
pkgname: string, installed: Installed[], actx: ActionContext,
|
|
760
768
|
) {
|
|
@@ -938,6 +946,7 @@ export {
|
|
|
938
946
|
package_add,
|
|
939
947
|
package_update,
|
|
940
948
|
installedFrom,
|
|
949
|
+
npmFetchArgs,
|
|
941
950
|
resolvePackage,
|
|
942
951
|
selectItems,
|
|
943
952
|
parseAliases,
|