@voxgig/sdkgen 4.23.0 → 4.24.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 +18 -2
- package/dist/action/doctor.d.ts +1 -1
- package/dist/action/doctor.js +12 -10
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +4 -0
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +5 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/remove.d.ts +16 -0
- package/dist/action/remove.js +345 -0
- package/dist/action/remove.js.map +1 -0
- package/dist/action/target.js +6 -1
- package/dist/action/target.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/c/core/context.c +3 -1
- package/project/.sdk/tm/c/feature/paging.c +37 -3
- package/project/.sdk/tm/c/feature/test.c +51 -27
- package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
- package/project/.sdk/tm/c/utility/transform_request.c +25 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
- package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
- package/project/.sdk/tm/cpp/core/types.hpp +1 -1
- package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
- package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
- package/project/.sdk/tm/csharp/core/Context.cs +1 -1
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
- package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
- package/project/.sdk/tm/go/core/context.go +13 -13
- package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
- package/project/.sdk/tm/go/feature/test_feature.go +3 -15
- package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
- package/project/.sdk/tm/go/test/feature_test.go +54 -0
- package/project/.sdk/tm/go/utility/fetcher.go +41 -20
- package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
- package/project/.sdk/tm/go/utility/transform_request.go +22 -3
- package/project/.sdk/tm/java/core/Context.java +2 -1
- package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
- package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
- package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
- package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
- package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
- package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
- package/project/.sdk/tm/js/src/Context.js +7 -2
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
- package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
- package/project/.sdk/tm/lua/core/context.lua +8 -1
- package/project/.sdk/tm/lua/core/control.lua +3 -0
- package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
- package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
- package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
- package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
- package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
- package/project/.sdk/tm/perl/core/context.pm +2 -1
- package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
- package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
- package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
- package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
- package/project/.sdk/tm/php/core/Context.php +5 -1
- package/project/.sdk/tm/php/core/Control.php +5 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
- package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
- package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
- package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
- package/project/.sdk/tm/py/pkg/core/context.py +2 -1
- package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
- package/project/.sdk/tm/py/test/test_feature.py +27 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
- package/project/.sdk/tm/rb/core/context.rb +1 -1
- package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
- package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
- package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
- package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
- package/project/.sdk/tm/rust/core/context.rs +2 -6
- package/project/.sdk/tm/rust/feature/paging.rs +44 -3
- package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
- package/project/.sdk/tm/rust/feature/test.rs +66 -33
- package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
- package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
- package/project/.sdk/tm/scala/core/Context.scala +2 -1
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
- package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
- package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
- package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
- package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
- package/project/.sdk/tm/ts/src/Context.ts +7 -2
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
- package/project/.sdk/tm/zig/core/context.zig +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +19 -5
- package/project/.sdk/tm/zig/feature/paging.zig +34 -4
- package/project/.sdk/tm/zig/feature/test.zig +54 -26
- package/project/sdkgen-package.json +1 -1
- package/src/action/doctor.ts +12 -9
- package/src/action/edition.ts +8 -0
- package/src/action/feature.ts +7 -1
- package/src/action/remove.ts +492 -0
- package/src/action/target.ts +8 -1
|
@@ -164,24 +164,12 @@ func (f *TestFeature) Init(ctx *core.Context, options map[string]any) {
|
|
|
164
164
|
args := self.buildArgs(ctx, op, updateMatch)
|
|
165
165
|
found := vs.Select(entmap, args)
|
|
166
166
|
ent := vs.GetElem(found, 0)
|
|
167
|
-
if ent == nil && entmap != nil {
|
|
168
|
-
for _, e := range entmap {
|
|
169
|
-
if _, ok := e.(map[string]any); ok {
|
|
170
|
-
ent = e
|
|
171
|
-
break
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
167
|
if ent == nil {
|
|
168
|
+
// update miss: 404, never another record
|
|
176
169
|
return respond(404, nil, map[string]any{"statusText": "Not found"}), nil
|
|
177
170
|
}
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
if reqdata != nil {
|
|
181
|
-
for k, v := range reqdata {
|
|
182
|
-
entm[k] = v
|
|
183
|
-
}
|
|
184
|
-
}
|
|
171
|
+
if _, ok := ent.(map[string]any); ok && ctx.Reqdata != nil {
|
|
172
|
+
vs.Merge([]any{ent, ctx.Reqdata})
|
|
185
173
|
}
|
|
186
174
|
vs.DelProp(ent, "$KEY")
|
|
187
175
|
out := vs.Clone(ent)
|
|
@@ -428,6 +428,7 @@ func TestFeatureCorpus(t *testing.T) {
|
|
|
428
428
|
"feature": []any{map[string]any{"name": name, "active": true}},
|
|
429
429
|
})
|
|
430
430
|
if fcRecord(probe, name) == nil {
|
|
431
|
+
t.Logf("feature.%s: inert (this SDK does not generate the feature)", name)
|
|
431
432
|
t.Skipf("this SDK was generated without the %s feature", name)
|
|
432
433
|
}
|
|
433
434
|
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"os"
|
|
6
6
|
"reflect"
|
|
7
7
|
"regexp"
|
|
8
|
+
"strconv"
|
|
8
9
|
"strings"
|
|
9
10
|
"testing"
|
|
10
11
|
"time"
|
|
@@ -1042,6 +1043,59 @@ func TestFeaturePaging(t *testing.T) {
|
|
|
1042
1043
|
}
|
|
1043
1044
|
})
|
|
1044
1045
|
|
|
1046
|
+
t.Run("snake-case-signals-and-ctrl-write-back", func(t *testing.T) {
|
|
1047
|
+
rec := &fhRecorder{reply: func(n int, _ map[string]any) (any, error) {
|
|
1048
|
+
if n == 1 {
|
|
1049
|
+
return fhResponse(200, map[string]any{"has_more": true, "next_cursor": "c2"}, nil), nil
|
|
1050
|
+
}
|
|
1051
|
+
return fhResponse(200, map[string]any{"has_more": false}, nil), nil
|
|
1052
|
+
}}
|
|
1053
|
+
h := fhMake(rec.fetch, fhF(feat.NewPagingFeature(), nil))
|
|
1054
|
+
pg := map[string]any{}
|
|
1055
|
+
ctrl := map[string]any{"paging": pg}
|
|
1056
|
+
res := h.op(fhOpSpec{op: "list", path: "/w", ctrl: ctrl})
|
|
1057
|
+
if res.result.Paging["hasMore"] != true {
|
|
1058
|
+
t.Errorf("expected has_more read, got %v", res.result.Paging["hasMore"])
|
|
1059
|
+
}
|
|
1060
|
+
if res.result.Paging["cursor"] != "c2" {
|
|
1061
|
+
t.Errorf("expected next_cursor read, got %v", res.result.Paging["cursor"])
|
|
1062
|
+
}
|
|
1063
|
+
if pg["cursor"] != "c2" || pg["hasMore"] != true {
|
|
1064
|
+
t.Errorf("expected record written back into ctrl, got %v", pg)
|
|
1065
|
+
}
|
|
1066
|
+
h.op(fhOpSpec{op: "list", path: "/w", ctrl: ctrl})
|
|
1067
|
+
if !strings.Contains(rec.url(1), "cursor=c2") {
|
|
1068
|
+
t.Errorf("expected written-back cursor sent, got %s", rec.url(1))
|
|
1069
|
+
}
|
|
1070
|
+
if pg["hasMore"] != false || pg["cursor"] != nil {
|
|
1071
|
+
t.Errorf("expected ctrl to carry the last record, got %v", pg)
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
1074
|
+
|
|
1075
|
+
t.Run("continues-from-written-back-next-page", func(t *testing.T) {
|
|
1076
|
+
rec := &fhRecorder{reply: func(n int, _ map[string]any) (any, error) {
|
|
1077
|
+
body := map[string]any{}
|
|
1078
|
+
if n == 1 {
|
|
1079
|
+
body["next_page"] = 2
|
|
1080
|
+
}
|
|
1081
|
+
return fhResponse(200, body, map[string]any{"x-page": strconv.Itoa(n)}), nil
|
|
1082
|
+
}}
|
|
1083
|
+
h := fhMake(rec.fetch, fhF(feat.NewPagingFeature(), nil))
|
|
1084
|
+
pg := map[string]any{}
|
|
1085
|
+
ctrl := map[string]any{"paging": pg}
|
|
1086
|
+
h.op(fhOpSpec{op: "list", path: "/w", ctrl: ctrl})
|
|
1087
|
+
if pg["page"] != 1 || pg["nextPage"] != 2 || pg["hasMore"] != true {
|
|
1088
|
+
t.Errorf("expected next_page read and written back, got %v", pg)
|
|
1089
|
+
}
|
|
1090
|
+
h.op(fhOpSpec{op: "list", path: "/w", ctrl: ctrl})
|
|
1091
|
+
if !strings.Contains(rec.url(1), "page=2") {
|
|
1092
|
+
t.Errorf("expected written-back next page sent, got %s", rec.url(1))
|
|
1093
|
+
}
|
|
1094
|
+
if pg["hasMore"] != false {
|
|
1095
|
+
t.Errorf("expected no more pages, got %v", pg)
|
|
1096
|
+
}
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1045
1099
|
t.Run("non-list-not-paged", func(t *testing.T) {
|
|
1046
1100
|
rec := &fhRecorder{}
|
|
1047
1101
|
h := fhMake(rec.fetch, fhF(feat.NewPagingFeature(), nil))
|
|
@@ -6,12 +6,52 @@ import (
|
|
|
6
6
|
"net/http"
|
|
7
7
|
"net/url"
|
|
8
8
|
"strings"
|
|
9
|
+
"sync"
|
|
9
10
|
|
|
10
11
|
vs "github.com/voxgig/struct"
|
|
11
12
|
|
|
12
13
|
"GOMODULE/core"
|
|
13
14
|
)
|
|
14
15
|
|
|
16
|
+
// Proxied clients keyed by proxy URL. A client per request would open a
|
|
17
|
+
// fresh connection every call; a cached one pools like http.DefaultClient.
|
|
18
|
+
var proxyClients sync.Map
|
|
19
|
+
|
|
20
|
+
func clientFor(fetchdef map[string]any) *http.Client {
|
|
21
|
+
client := http.DefaultClient
|
|
22
|
+
|
|
23
|
+
if proxy, ok := fetchdef["proxy"].(string); ok && proxy != "" {
|
|
24
|
+
if cached, ok := proxyClients.Load(proxy); ok {
|
|
25
|
+
client = cached.(*http.Client)
|
|
26
|
+
} else if proxyURL, perr := url.Parse(proxy); perr == nil {
|
|
27
|
+
// http.DefaultTransport is a package variable the host program can
|
|
28
|
+
// replace, and an unchecked assertion on it panics the SDK inside
|
|
29
|
+
// whatever called it. Clone the real one where it is there, and
|
|
30
|
+
// build a plain transport where it is not.
|
|
31
|
+
var transport *http.Transport
|
|
32
|
+
if def, ok := http.DefaultTransport.(*http.Transport); ok {
|
|
33
|
+
transport = def.Clone()
|
|
34
|
+
} else {
|
|
35
|
+
transport = &http.Transport{}
|
|
36
|
+
}
|
|
37
|
+
transport.Proxy = http.ProxyURL(proxyURL)
|
|
38
|
+
cached, _ := proxyClients.LoadOrStore(proxy, &http.Client{Transport: transport})
|
|
39
|
+
client = cached.(*http.Client)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A shallow copy shares the pooled Transport; only CheckRedirect differs.
|
|
44
|
+
if redirect, ok := fetchdef["redirect"].(string); ok && redirect == "manual" {
|
|
45
|
+
manual := *client
|
|
46
|
+
manual.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
47
|
+
return http.ErrUseLastResponse
|
|
48
|
+
}
|
|
49
|
+
client = &manual
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return client
|
|
53
|
+
}
|
|
54
|
+
|
|
15
55
|
func defaultHTTPFetch(fullurl string, fetchdef map[string]any) (map[string]any, error) {
|
|
16
56
|
method, _ := fetchdef["method"].(string)
|
|
17
57
|
if method == "" {
|
|
@@ -46,26 +86,7 @@ func defaultHTTPFetch(fullurl string, fetchdef map[string]any) (map[string]any,
|
|
|
46
86
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
|
|
47
87
|
}
|
|
48
88
|
|
|
49
|
-
|
|
50
|
-
// feature): route the request through an http.Transport with Proxy set.
|
|
51
|
-
client := http.DefaultClient
|
|
52
|
-
if proxy, ok := fetchdef["proxy"].(string); ok && proxy != "" {
|
|
53
|
-
if proxyURL, perr := url.Parse(proxy); perr == nil {
|
|
54
|
-
client = &http.Client{
|
|
55
|
-
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if redirect, ok := fetchdef["redirect"].(string); ok && redirect == "manual" {
|
|
61
|
-
manual := *client
|
|
62
|
-
manual.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
63
|
-
return http.ErrUseLastResponse
|
|
64
|
-
}
|
|
65
|
-
client = &manual
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
resp, err := client.Do(req)
|
|
89
|
+
resp, err := clientFor(fetchdef).Do(req)
|
|
69
90
|
if err != nil {
|
|
70
91
|
return nil, err
|
|
71
92
|
}
|
|
@@ -29,7 +29,7 @@ func prepareQueryUtil(ctx *core.Context) map[string]any {
|
|
|
29
29
|
for _, item := range vs.Items(reqmatch) {
|
|
30
30
|
key, _ := item[0].(string)
|
|
31
31
|
val := item[1]
|
|
32
|
-
if val != nil && !containsStr(params, key) {
|
|
32
|
+
if val != nil && key != "$action" && !containsStr(params, key) {
|
|
33
33
|
out[key] = val
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -16,12 +16,12 @@ func transformRequestUtil(ctx *core.Context) any {
|
|
|
16
16
|
|
|
17
17
|
transform := core.ToMapAny(vs.GetProp(point, "transform"))
|
|
18
18
|
if transform == nil {
|
|
19
|
-
return ctx.Reqdata
|
|
19
|
+
return stripAction(ctx.Reqdata)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
reqform := vs.GetProp(transform, "req")
|
|
23
23
|
if reqform == nil {
|
|
24
|
-
return ctx.Reqdata
|
|
24
|
+
return stripAction(ctx.Reqdata)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
reqdata, terr := vs.Transform(map[string]any{
|
|
@@ -36,5 +36,24 @@ func transformRequestUtil(ctx *core.Context) any {
|
|
|
36
36
|
return terr
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
return reqdata
|
|
39
|
+
return stripAction(reqdata)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `$action` selects the point (see makePointUtil); it is never an API field,
|
|
43
|
+
// so the body is a copy without it. The caller's map is left untouched.
|
|
44
|
+
func stripAction(reqdata any) any {
|
|
45
|
+
src, ok := reqdata.(map[string]any)
|
|
46
|
+
if !ok {
|
|
47
|
+
return reqdata
|
|
48
|
+
}
|
|
49
|
+
if _, has := src["$action"]; !has {
|
|
50
|
+
return reqdata
|
|
51
|
+
}
|
|
52
|
+
body := make(map[string]any, len(src))
|
|
53
|
+
for k, v := range src {
|
|
54
|
+
if k != "$action" {
|
|
55
|
+
body[k] = v
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return body
|
|
40
59
|
}
|
|
@@ -82,7 +82,8 @@ public class Context {
|
|
|
82
82
|
this.ctrl = (Control) cv;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
else if (basectx != null && basectx.ctrl != null
|
|
85
|
+
else if (basectx != null && basectx.ctrl != null
|
|
86
|
+
&& Helpers.getCtxProp(ctxmap, "opname") == null) {
|
|
86
87
|
this.ctrl = basectx.ctrl;
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -18,8 +18,8 @@ import JAVAPACKAGE.utility.struct.Struct;
|
|
|
18
18
|
// stamps page/limit (or a cursor) into the request query; on the way back
|
|
19
19
|
// (PreResult) it reads the server's pagination signals — a `Link:
|
|
20
20
|
// rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
|
|
21
|
-
// `next`/`cursor`/`nextCursor`/`hasMore` fields in the body
|
|
22
|
-
// them on `ctx.result.paging`. A per-call cursor/page from ctrl takes
|
|
21
|
+
// `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case
|
|
22
|
+
// forms too) — and records them on `ctx.result.paging` and `ctrl.paging`. A per-call cursor/page from ctrl takes
|
|
23
23
|
// priority (used by auto-iteration). Parameter names (`pageParam`,
|
|
24
24
|
// `limitParam`, `cursorParam`), the page size (`limit`) and the start page
|
|
25
25
|
// (`startPage`, default 1) are configurable.
|
|
@@ -82,7 +82,12 @@ public class PagingFeature extends BaseFeature {
|
|
|
82
82
|
spec.query.put(cursorParam, cursor);
|
|
83
83
|
}
|
|
84
84
|
else if (spec.query.get(pageParam) == null) {
|
|
85
|
-
|
|
85
|
+
// A record written back by preResult holds the page just fetched as
|
|
86
|
+
// "page" and the one to fetch as "nextPage", so nextPage wins.
|
|
87
|
+
Object page = paging.get("nextPage");
|
|
88
|
+
if (page == null) {
|
|
89
|
+
page = paging.get("page");
|
|
90
|
+
}
|
|
86
91
|
if (page != null) {
|
|
87
92
|
spec.query.put(pageParam, page);
|
|
88
93
|
}
|
|
@@ -206,18 +211,37 @@ public class PagingFeature extends BaseFeature {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
// Body-level
|
|
214
|
+
// Body-level signals; snake_case forms are read first so camelCase wins.
|
|
210
215
|
if (body instanceof Map) {
|
|
211
216
|
Map<String, Object> bm = (Map<String, Object>) body;
|
|
212
217
|
if (bm.get("next") != null && paging.get("next") == null) {
|
|
213
218
|
paging.put("next", bm.get("next"));
|
|
214
219
|
}
|
|
220
|
+
if (bm.get("next_cursor") != null) {
|
|
221
|
+
paging.put("cursor", bm.get("next_cursor"));
|
|
222
|
+
}
|
|
215
223
|
if (bm.get("cursor") != null) {
|
|
216
224
|
paging.put("cursor", bm.get("cursor"));
|
|
217
225
|
}
|
|
218
226
|
if (bm.get("nextCursor") != null) {
|
|
219
227
|
paging.put("cursor", bm.get("nextCursor"));
|
|
220
228
|
}
|
|
229
|
+
if (paging.get("nextPage") == null) {
|
|
230
|
+
Object np = bm.get("nextPage");
|
|
231
|
+
if (np == null) {
|
|
232
|
+
np = bm.get("next_page");
|
|
233
|
+
}
|
|
234
|
+
if (np instanceof Number) {
|
|
235
|
+
paging.put("nextPage", ((Number) np).intValue());
|
|
236
|
+
}
|
|
237
|
+
else if (np instanceof String) {
|
|
238
|
+
paging.put("nextPage", np);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (bm.get("has_more") instanceof Boolean) {
|
|
242
|
+
paging.put("hasMore", bm.get("has_more"));
|
|
243
|
+
explicitMore = true;
|
|
244
|
+
}
|
|
221
245
|
if (bm.get("hasMore") instanceof Boolean) {
|
|
222
246
|
paging.put("hasMore", bm.get("hasMore"));
|
|
223
247
|
explicitMore = true;
|
|
@@ -237,6 +261,24 @@ public class PagingFeature extends BaseFeature {
|
|
|
237
261
|
|
|
238
262
|
result.paging = paging;
|
|
239
263
|
this.last = paging;
|
|
264
|
+
|
|
265
|
+
if (ctx.ctrl != null) {
|
|
266
|
+
// The Context shares the caller's paging map by reference, so refill
|
|
267
|
+
// it in place; an immutable map falls back to replacing the reference.
|
|
268
|
+
Map<String, Object> shared = ctx.ctrl.paging;
|
|
269
|
+
if (shared == null) {
|
|
270
|
+
ctx.ctrl.paging = paging;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
try {
|
|
274
|
+
shared.clear();
|
|
275
|
+
shared.putAll(paging);
|
|
276
|
+
}
|
|
277
|
+
catch (UnsupportedOperationException e) {
|
|
278
|
+
ctx.ctrl.paging = paging;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
240
282
|
}
|
|
241
283
|
|
|
242
284
|
private boolean isList(Context ctx) {
|
|
@@ -228,19 +228,12 @@ public class TestFeature extends BaseFeature {
|
|
|
228
228
|
Object args = buildArgs(ctx, op, updateMatch);
|
|
229
229
|
List<Object> found = Struct.select(entmap, args);
|
|
230
230
|
Object ent = Struct.getelem(found, 0);
|
|
231
|
-
if (ent == null && entmap != null) {
|
|
232
|
-
for (Object e : entmap.values()) {
|
|
233
|
-
if (e instanceof Map) {
|
|
234
|
-
ent = e;
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
231
|
if (ent == null) {
|
|
232
|
+
// update miss: 404, never another record
|
|
240
233
|
return respond(ctx, 404, null, extra("statusText", "Not found"));
|
|
241
234
|
}
|
|
242
235
|
if (ent instanceof Map && ctx.reqdata != null) {
|
|
243
|
-
((
|
|
236
|
+
Struct.merge(Struct.jt(ent, ctx.reqdata));
|
|
244
237
|
}
|
|
245
238
|
Struct.delprop(ent, "$KEY");
|
|
246
239
|
Object out = Struct.clone(ent);
|
|
@@ -374,10 +367,14 @@ public class TestFeature extends BaseFeature {
|
|
|
374
367
|
}
|
|
375
368
|
}
|
|
376
369
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
Object
|
|
380
|
-
|
|
370
|
+
// Path AND query: a path-only read misses a query-addressed record
|
|
371
|
+
// (e.g. GET /result?trace_id=), which has no path param at all.
|
|
372
|
+
List<Object> reqdArgs = Struct.jt();
|
|
373
|
+
for (String kind : new String[] { "params", "query" }) {
|
|
374
|
+
Object argsPath = Struct.getpath(point, List.of("args", kind));
|
|
375
|
+
reqdArgs.addAll(Struct.select(argsPath, Struct.jm("reqd", true)));
|
|
376
|
+
}
|
|
377
|
+
Object reqd = Struct.transform(reqdArgs,
|
|
381
378
|
Struct.jt("`$EACH`", "", "`$KEY.name`"));
|
|
382
379
|
|
|
383
380
|
List<Object> qand = Struct.jt();
|
|
@@ -405,6 +405,9 @@ public class FeatureCorpusTest {
|
|
|
405
405
|
ProjectNameSDK probe = buildClient(Map.of("feature",
|
|
406
406
|
List.of(Map.of("name", name, "active", true))));
|
|
407
407
|
if (null == record(probe, name)) {
|
|
408
|
+
// The one line every runner prints for an inert section.
|
|
409
|
+
System.err.println(String.format(
|
|
410
|
+
"feature.%s: inert (this SDK does not generate the feature)", name));
|
|
408
411
|
continue;
|
|
409
412
|
}
|
|
410
413
|
|
|
@@ -867,6 +867,48 @@ public class FeatureTest {
|
|
|
867
867
|
assertEquals(true, res.result.paging.get("hasMore"), "expected hasMore");
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
@Test
|
|
871
|
+
public void paging_snakeCaseSignalsAndCtrlWriteBack() {
|
|
872
|
+
assumeFeatures("paging");
|
|
873
|
+
FhRecorder rec = new FhRecorder();
|
|
874
|
+
rec.reply = (n, fetchdef) -> 1 == n
|
|
875
|
+
? fhResponse(200, fhMap("has_more", true, "next_cursor", "c2"), null)
|
|
876
|
+
: fhResponse(200, fhMap("has_more", false), null);
|
|
877
|
+
FhHarness h = fhMake(rec::fetch, fhF(new PagingFeature(), null));
|
|
878
|
+
Map<String, Object> pg = new LinkedHashMap<>();
|
|
879
|
+
Map<String, Object> ctrl = fhMap("paging", pg);
|
|
880
|
+
FhOpResult res = h.op(fhOp("list").path("/w").ctrl(ctrl));
|
|
881
|
+
assertEquals(true, res.result.paging.get("hasMore"), "expected has_more read");
|
|
882
|
+
assertEquals("c2", res.result.paging.get("cursor"), "expected next_cursor read");
|
|
883
|
+
assertEquals("c2", pg.get("cursor"), "expected record written back into ctrl");
|
|
884
|
+
assertEquals(true, pg.get("hasMore"), "expected record written back into ctrl");
|
|
885
|
+
h.op(fhOp("list").path("/w").ctrl(ctrl));
|
|
886
|
+
assertTrue(rec.url(1).contains("cursor=c2"),
|
|
887
|
+
"expected written-back cursor sent: " + rec.url(1));
|
|
888
|
+
assertEquals(false, pg.get("hasMore"), "expected ctrl to carry the last record");
|
|
889
|
+
assertNull(pg.get("cursor"), "expected ctrl to carry the last record");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
@Test
|
|
893
|
+
public void paging_continuesFromWrittenBackNextPage() {
|
|
894
|
+
assumeFeatures("paging");
|
|
895
|
+
FhRecorder rec = new FhRecorder();
|
|
896
|
+
rec.reply = (n, fetchdef) -> fhResponse(200,
|
|
897
|
+
1 == n ? fhMap("next_page", 2) : fhMap(),
|
|
898
|
+
fhMap("x-page", String.valueOf(n)));
|
|
899
|
+
FhHarness h = fhMake(rec::fetch, fhF(new PagingFeature(), null));
|
|
900
|
+
Map<String, Object> pg = new LinkedHashMap<>();
|
|
901
|
+
Map<String, Object> ctrl = fhMap("paging", pg);
|
|
902
|
+
h.op(fhOp("list").path("/w").ctrl(ctrl));
|
|
903
|
+
assertEquals(1, pg.get("page"), "expected x-page written back");
|
|
904
|
+
assertEquals(2, pg.get("nextPage"), "expected next_page written back");
|
|
905
|
+
assertEquals(true, pg.get("hasMore"), "expected hasMore inferred from next_page");
|
|
906
|
+
h.op(fhOp("list").path("/w").ctrl(ctrl));
|
|
907
|
+
assertTrue(rec.url(1).contains("page=2"),
|
|
908
|
+
"expected written-back next page sent: " + rec.url(1));
|
|
909
|
+
assertEquals(false, pg.get("hasMore"), "expected no more pages");
|
|
910
|
+
}
|
|
911
|
+
|
|
870
912
|
@Test
|
|
871
913
|
public void paging_nonListNotPaged() {
|
|
872
914
|
assumeFeatures("paging");
|
|
@@ -9,6 +9,7 @@ import java.net.http.HttpResponse;
|
|
|
9
9
|
import java.util.LinkedHashMap;
|
|
10
10
|
import java.util.List;
|
|
11
11
|
import java.util.Map;
|
|
12
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
12
13
|
import java.util.function.BiFunction;
|
|
13
14
|
import java.util.function.Supplier;
|
|
14
15
|
|
|
@@ -20,6 +21,40 @@ final class Fetcher {
|
|
|
20
21
|
|
|
21
22
|
private Fetcher() {}
|
|
22
23
|
|
|
24
|
+
// Clients keyed by redirect policy and proxy. Each HttpClient owns a
|
|
25
|
+
// connection pool and a selector thread, so one per request reused
|
|
26
|
+
// nothing; one per key pools across calls.
|
|
27
|
+
private static final ConcurrentHashMap<String, HttpClient> CLIENTS =
|
|
28
|
+
new ConcurrentHashMap<>();
|
|
29
|
+
|
|
30
|
+
private static HttpClient clientFor(Map<String, Object> fetchdef) {
|
|
31
|
+
// "manual" (set by the station feature's middleware under a hosts
|
|
32
|
+
// policy) returns a 3xx like any other response: an automatic follow
|
|
33
|
+
// would carry injected credentials to a host no policy approved.
|
|
34
|
+
final HttpClient.Redirect redirectPolicy =
|
|
35
|
+
"manual".equals(fetchdef.get("redirect"))
|
|
36
|
+
? HttpClient.Redirect.NEVER
|
|
37
|
+
: HttpClient.Redirect.NORMAL;
|
|
38
|
+
|
|
39
|
+
Object praw = fetchdef.get("proxy");
|
|
40
|
+
final String proxy = praw instanceof String ? (String) praw : "";
|
|
41
|
+
|
|
42
|
+
return CLIENTS.computeIfAbsent(redirectPolicy + "|" + proxy, key -> {
|
|
43
|
+
HttpClient.Builder clientb = HttpClient.newBuilder().followRedirects(redirectPolicy);
|
|
44
|
+
if (!"".equals(proxy)) {
|
|
45
|
+
try {
|
|
46
|
+
URI proxyUri = URI.create(proxy);
|
|
47
|
+
int port = proxyUri.getPort() < 0 ? 80 : proxyUri.getPort();
|
|
48
|
+
clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), port)));
|
|
49
|
+
}
|
|
50
|
+
catch (RuntimeException e) {
|
|
51
|
+
// Unparseable proxy target: direct connection, as the go transport.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return clientb.build();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
static Map<String, Object> defaultHttpFetch(String fullurl, Map<String, Object> fetchdef) {
|
|
24
59
|
String method = fetchdef.get("method") instanceof String
|
|
25
60
|
? (String) fetchdef.get("method") : "";
|
|
@@ -59,36 +94,9 @@ final class Fetcher {
|
|
|
59
94
|
reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)");
|
|
60
95
|
}
|
|
61
96
|
|
|
62
|
-
// Honour a redirect annotation on the fetch definition (set by the
|
|
63
|
-
// station feature's middleware under a hosts policy): "manual" returns
|
|
64
|
-
// a 3xx like any other response instead of following it — the ts
|
|
65
|
-
// donor's fetch honours the same key natively, and an automatic follow
|
|
66
|
-
// would carry injected credentials to a host no policy approved.
|
|
67
|
-
HttpClient.Redirect redirectPolicy =
|
|
68
|
-
"manual".equals(fetchdef.get("redirect"))
|
|
69
|
-
? HttpClient.Redirect.NEVER
|
|
70
|
-
: HttpClient.Redirect.NORMAL;
|
|
71
|
-
|
|
72
|
-
// Honour a proxy annotation on the fetch definition (set by the proxy
|
|
73
|
-
// feature): route the request through a proxied HttpClient.
|
|
74
|
-
HttpClient.Builder clientb = HttpClient.newBuilder()
|
|
75
|
-
.followRedirects(redirectPolicy);
|
|
76
|
-
Object proxy = fetchdef.get("proxy");
|
|
77
|
-
if (proxy instanceof String && !"".equals(proxy)) {
|
|
78
|
-
try {
|
|
79
|
-
URI proxyUri = URI.create((String) proxy);
|
|
80
|
-
int port = proxyUri.getPort() < 0 ? 80 : proxyUri.getPort();
|
|
81
|
-
clientb.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), port)));
|
|
82
|
-
}
|
|
83
|
-
catch (RuntimeException e) {
|
|
84
|
-
// Unparseable proxy target: fall through to a direct connection,
|
|
85
|
-
// mirroring the go transport (bad proxy URL is ignored).
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
97
|
HttpResponse<String> resp;
|
|
90
98
|
try {
|
|
91
|
-
resp =
|
|
99
|
+
resp = clientFor(fetchdef).send(reqb.build(), HttpResponse.BodyHandlers.ofString());
|
|
92
100
|
}
|
|
93
101
|
catch (Exception e) {
|
|
94
102
|
throw new RuntimeException("fetch: " + e.getMessage(), e);
|
|
@@ -35,7 +35,7 @@ final class PrepareQuery {
|
|
|
35
35
|
for (List<Object> item : Struct.items(reqmatch)) {
|
|
36
36
|
String key = item.get(0) instanceof String ? (String) item.get(0) : "";
|
|
37
37
|
Object val = item.get(1);
|
|
38
|
-
if (val != null && !containsStr(params, key)) {
|
|
38
|
+
if (val != null && !"$action".equals(key) && !containsStr(params, key)) {
|
|
39
39
|
out.put(key, val);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -19,17 +19,32 @@ final class TransformRequest {
|
|
|
19
19
|
Map<String, Object> transform =
|
|
20
20
|
Helpers.toMapAny(Struct.getprop(ctx.point, "transform"));
|
|
21
21
|
if (transform == null) {
|
|
22
|
-
return ctx.reqdata;
|
|
22
|
+
return stripAction(ctx.reqdata);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
Object reqform = Struct.getprop(transform, "req", null);
|
|
26
26
|
if (reqform == null) {
|
|
27
|
-
return ctx.reqdata;
|
|
27
|
+
return stripAction(ctx.reqdata);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
31
31
|
data.put("reqdata", ctx.reqdata);
|
|
32
32
|
|
|
33
|
-
return Struct.transform(data, reqform);
|
|
33
|
+
return stripAction(Struct.transform(data, reqform));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// `$action` selects the point (see MakePoint); it is never an API field, so
|
|
37
|
+
// the body is a copy without it. The caller's map is left untouched.
|
|
38
|
+
private static Object stripAction(Object reqdata) {
|
|
39
|
+
if (!(reqdata instanceof Map) || !((Map<?, ?>) reqdata).containsKey("$action")) {
|
|
40
|
+
return reqdata;
|
|
41
|
+
}
|
|
42
|
+
Map<String, Object> body = new LinkedHashMap<>();
|
|
43
|
+
for (Map.Entry<?, ?> e : ((Map<?, ?>) reqdata).entrySet()) {
|
|
44
|
+
if (!"$action".equals(e.getKey())) {
|
|
45
|
+
body.put(String.valueOf(e.getKey()), e.getValue());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return body;
|
|
34
49
|
}
|
|
35
50
|
}
|
|
@@ -57,7 +57,13 @@ class Context {
|
|
|
57
57
|
this.client = getprop(ctxmap, 'client', getprop(basectx, 'client'))
|
|
58
58
|
this.utility = getprop(ctxmap, 'utility', getprop(basectx, 'utility'))
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// An operation gets its OWN control unless the caller passed one: a
|
|
61
|
+
// paging cursor left on the client's control would otherwise be picked
|
|
62
|
+
// up by the next, unrelated call. A context with no opname is not an
|
|
63
|
+
// operation, so it still shares (an entity's, with the client's).
|
|
64
|
+
const opname = getprop(ctxmap, 'opname')
|
|
65
|
+
const basectrl = null == opname ? getprop(basectx, 'ctrl', this.ctrl) : this.ctrl
|
|
66
|
+
this.ctrl = getprop(ctxmap, 'ctrl', basectrl)
|
|
61
67
|
this.meta = getprop(ctxmap, 'meta', getprop(basectx, 'meta', this.meta))
|
|
62
68
|
|
|
63
69
|
this.config = getprop(ctxmap, 'config', getprop(basectx, 'config'))
|
|
@@ -78,7 +84,6 @@ class Context {
|
|
|
78
84
|
this.result = getprop(ctxmap, 'result', getprop(basectx, 'result'))
|
|
79
85
|
this.response = getprop(ctxmap, 'response', getprop(basectx, 'response'))
|
|
80
86
|
|
|
81
|
-
const opname = getprop(ctxmap, 'opname')
|
|
82
87
|
this.op = this.resolveOp(opname)
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -7,8 +7,9 @@ const { BaseFeature } = require('../base/BaseFeature')
|
|
|
7
7
|
// stamps page/limit (or a cursor) into the request query; on the way back
|
|
8
8
|
// (PreResult) it reads the server's pagination signals — a `Link:
|
|
9
9
|
// rel="next"` header, `X-Next-Page`/`X-Total-Count` headers, or `next`/
|
|
10
|
-
// `cursor`/`hasMore` fields in the body
|
|
11
|
-
// `ctx.result.paging
|
|
10
|
+
// `cursor`/`hasMore` fields in the body (snake_case forms too) — and
|
|
11
|
+
// records them on `ctx.result.paging` and the caller's `ctrl.paging`.
|
|
12
|
+
// Generated SDKs build auto-iteration on top of this
|
|
12
13
|
// (advance the cursor/page and re-issue the list call until `hasMore` is
|
|
13
14
|
// false). Parameter names and page size are configurable.
|
|
14
15
|
class PagingFeature extends BaseFeature {
|
|
@@ -28,7 +29,7 @@ class PagingFeature extends BaseFeature {
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
PreRequest(ctx) {
|
|
31
|
-
if (!this._isList(ctx)) {
|
|
32
|
+
if (!this.active || !this._isList(ctx)) {
|
|
32
33
|
return
|
|
33
34
|
}
|
|
34
35
|
const spec = ctx.spec
|
|
@@ -58,7 +59,10 @@ class PagingFeature extends BaseFeature {
|
|
|
58
59
|
spec.query[cursorParam] = paging.cursor
|
|
59
60
|
}
|
|
60
61
|
else if (null == spec.query[pageParam]) {
|
|
61
|
-
|
|
62
|
+
// A record written back by PreResult holds the page just fetched as
|
|
63
|
+
// `page` and the one to fetch as `nextPage`, so nextPage wins.
|
|
64
|
+
spec.query[pageParam] = null != paging.nextPage ? paging.nextPage :
|
|
65
|
+
null != paging.page ? paging.page : (this._options.startPage || 1)
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
if (null != this._options.limit && null == spec.query[limitParam]) {
|
|
@@ -68,7 +72,7 @@ class PagingFeature extends BaseFeature {
|
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
PreResult(ctx) {
|
|
71
|
-
if (!this._isList(ctx)) {
|
|
75
|
+
if (!this.active || !this._isList(ctx)) {
|
|
72
76
|
return
|
|
73
77
|
}
|
|
74
78
|
const result = ctx.result
|
|
@@ -124,11 +128,20 @@ class PagingFeature extends BaseFeature {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
// Body-level
|
|
131
|
+
// Body-level signals; snake_case forms are read first so camelCase wins.
|
|
128
132
|
if (body && 'object' === typeof body) {
|
|
129
133
|
if (null != body.next) { paging.next = paging.next || body.next }
|
|
134
|
+
if (null != body.next_cursor) { paging.cursor = body.next_cursor }
|
|
130
135
|
if (null != body.cursor) { paging.cursor = body.cursor }
|
|
131
136
|
if (null != body.nextCursor) { paging.cursor = body.nextCursor }
|
|
137
|
+
if (null == paging.nextPage) {
|
|
138
|
+
const np = null != body.nextPage ? body.nextPage : body.next_page
|
|
139
|
+
if ('number' === typeof np || 'string' === typeof np) { paging.nextPage = np }
|
|
140
|
+
}
|
|
141
|
+
if ('boolean' === typeof body.has_more) {
|
|
142
|
+
paging.hasMore = body.has_more
|
|
143
|
+
explicitMore = true
|
|
144
|
+
}
|
|
132
145
|
if ('boolean' === typeof body.hasMore) {
|
|
133
146
|
paging.hasMore = body.hasMore
|
|
134
147
|
explicitMore = true
|
|
@@ -147,6 +160,11 @@ class PagingFeature extends BaseFeature {
|
|
|
147
160
|
|
|
148
161
|
result.paging = paging
|
|
149
162
|
|
|
163
|
+
// ctx.ctrl is the caller's own object, so the record reaches them.
|
|
164
|
+
if (null != ctx.ctrl) {
|
|
165
|
+
ctx.ctrl.paging = paging
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
const client = this._client
|
|
151
169
|
client._paging = { last: paging }
|
|
152
170
|
}
|