@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.
Files changed (138) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/package.json +1 -1
  16. package/project/.sdk/tm/c/core/context.c +3 -1
  17. package/project/.sdk/tm/c/feature/paging.c +37 -3
  18. package/project/.sdk/tm/c/feature/test.c +51 -27
  19. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  20. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  21. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  22. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  23. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  24. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  25. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  26. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  27. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  28. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  29. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  30. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  31. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  32. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  33. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  34. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  35. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  36. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  37. package/project/.sdk/tm/go/core/context.go +13 -13
  38. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  39. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  40. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  41. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  42. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  43. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  44. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  45. package/project/.sdk/tm/java/core/Context.java +2 -1
  46. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  47. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  48. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  49. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  50. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  51. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  52. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  53. package/project/.sdk/tm/js/src/Context.js +7 -2
  54. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  55. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  56. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  57. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  58. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  59. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  60. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  61. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  62. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  63. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  64. package/project/.sdk/tm/lua/core/context.lua +8 -1
  65. package/project/.sdk/tm/lua/core/control.lua +3 -0
  66. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  67. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  68. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  69. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  70. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  71. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  72. package/project/.sdk/tm/perl/core/context.pm +2 -1
  73. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  74. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  75. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  76. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  77. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  78. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  79. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  80. package/project/.sdk/tm/php/core/Context.php +5 -1
  81. package/project/.sdk/tm/php/core/Control.php +5 -0
  82. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  83. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  84. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  85. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  86. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  87. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  88. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  89. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  90. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  91. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  92. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  93. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  94. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  95. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  96. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  97. package/project/.sdk/tm/rb/core/context.rb +1 -1
  98. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  99. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  100. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  101. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  102. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  103. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  104. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  105. package/project/.sdk/tm/rust/core/context.rs +2 -6
  106. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  107. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  108. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  109. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  110. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  111. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  112. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  113. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  114. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  115. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  116. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  117. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  118. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  119. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  120. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  121. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  122. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  123. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  124. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  125. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  126. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  127. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  128. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  129. package/project/.sdk/tm/zig/core/context.zig +1 -1
  130. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  131. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  132. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  133. package/project/sdkgen-package.json +1 -1
  134. package/src/action/doctor.ts +12 -9
  135. package/src/action/edition.ts +8 -0
  136. package/src/action/feature.ts +7 -1
  137. package/src/action/remove.ts +492 -0
  138. package/src/action/target.ts +8 -1
@@ -2,8 +2,8 @@
2
2
  // stamps page/limit (or a cursor) into the request query; on the way back
3
3
  // (PreResult) it reads the server's pagination signals - a `Link:
4
4
  // rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
5
- // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body - and records
6
- // them on `ctx.Result.Paging`. A per-call cursor/page from ctrl takes
5
+ // `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case
6
+ // forms too) - and records them on `ctx.Result.Paging` and `Ctrl.Paging`. A per-call cursor/page from ctrl takes
7
7
  // priority (used by auto-iteration). Parameter names (`pageParam`,
8
8
  // `limitParam`, `cursorParam`), the page size (`limit`) and the start page
9
9
  // (`startPage`, default 1) are configurable.
@@ -79,8 +79,12 @@ public class PagingFeature : BaseFeature
79
79
  }
80
80
  else if (!spec.Query.TryGetValue(pageParam, out var existing) || existing == null)
81
81
  {
82
+ // A record written back by PreResult holds the page just fetched
83
+ // as "page" and the one to fetch as "nextPage", so nextPage wins.
82
84
  object? page = null;
83
- var hasPage = paging != null && paging.TryGetValue("page", out page) && page != null;
85
+ var hasPage = paging != null &&
86
+ ((paging.TryGetValue("nextPage", out page) && page != null) ||
87
+ (paging.TryGetValue("page", out page) && page != null));
84
88
  spec.Query[pageParam] = hasPage ? page : FoptInt(_options, "startPage", 1);
85
89
  }
86
90
 
@@ -224,7 +228,7 @@ public class PagingFeature : BaseFeature
224
228
  }
225
229
  }
226
230
 
227
- // Body-level cursors.
231
+ // Body-level signals; snake_case forms are read first so camelCase wins.
228
232
  if (body is Dictionary<string, object?> bm)
229
233
  {
230
234
  if (bm.TryGetValue("next", out var next) && next != null &&
@@ -232,6 +236,10 @@ public class PagingFeature : BaseFeature
232
236
  {
233
237
  paging["next"] = next;
234
238
  }
239
+ if (bm.TryGetValue("next_cursor", out var snakeCursor) && snakeCursor != null)
240
+ {
241
+ paging["cursor"] = snakeCursor;
242
+ }
235
243
  if (bm.TryGetValue("cursor", out var cursor) && cursor != null)
236
244
  {
237
245
  paging["cursor"] = cursor;
@@ -240,6 +248,34 @@ public class PagingFeature : BaseFeature
240
248
  {
241
249
  paging["cursor"] = nextCursor;
242
250
  }
251
+ if (!paging.TryGetValue("nextPage", out var np0) || np0 == null)
252
+ {
253
+ bm.TryGetValue("nextPage", out var np);
254
+ if (np == null)
255
+ {
256
+ bm.TryGetValue("next_page", out np);
257
+ }
258
+ switch (np)
259
+ {
260
+ case int n:
261
+ paging["nextPage"] = n;
262
+ break;
263
+ case long n:
264
+ paging["nextPage"] = (int)n;
265
+ break;
266
+ case double n:
267
+ paging["nextPage"] = (int)n;
268
+ break;
269
+ case string s:
270
+ paging["nextPage"] = s;
271
+ break;
272
+ }
273
+ }
274
+ if (bm.TryGetValue("has_more", out var snakeMore) && snakeMore is bool smb)
275
+ {
276
+ paging["hasMore"] = smb;
277
+ explicitMore = true;
278
+ }
243
279
  if (bm.TryGetValue("hasMore", out var hasMore) && hasMore is bool hmb)
244
280
  {
245
281
  paging["hasMore"] = hmb;
@@ -262,6 +298,24 @@ public class PagingFeature : BaseFeature
262
298
 
263
299
  result.Paging = paging;
264
300
  Last = paging;
301
+
302
+ if (ctx.Ctrl != null)
303
+ {
304
+ // The Context shares the caller's paging map by reference, so
305
+ // refill it in place; a fresh map on Ctrl never reaches the caller.
306
+ if (ctx.Ctrl.Paging != null)
307
+ {
308
+ ctx.Ctrl.Paging.Clear();
309
+ foreach (var kv in paging)
310
+ {
311
+ ctx.Ctrl.Paging[kv.Key] = kv.Value;
312
+ }
313
+ }
314
+ else
315
+ {
316
+ ctx.Ctrl.Paging = paging;
317
+ }
318
+ }
265
319
  }
266
320
 
267
321
  private bool IsList(Context ctx)
@@ -188,30 +188,17 @@ public class TestFeature : BaseFeature
188
188
  var args = BuildArgs(ctx2, op, updateMatch);
189
189
  var found = StructUtils.Select(entmap, args);
190
190
  var ent = StructUtils.GetElem(found, 0);
191
- if (ent == null && entmap != null)
192
- {
193
- foreach (var e in entmap.Values)
194
- {
195
- if (e is Dictionary<string, object?>)
196
- {
197
- ent = e;
198
- break;
199
- }
200
- }
201
- }
202
191
  if (ent == null)
203
192
  {
193
+ // update miss: 404, never another record
204
194
  return Respond(404, null, new Dictionary<string, object?>
205
195
  {
206
196
  ["statusText"] = "Not found",
207
197
  });
208
198
  }
209
- if (ent is Dictionary<string, object?> entm && ctx2.Reqdata != null)
199
+ if (ent is Dictionary<string, object?> && ctx2.Reqdata != null)
210
200
  {
211
- foreach (var kv in ctx2.Reqdata)
212
- {
213
- entm[kv.Key] = kv.Value;
214
- }
201
+ StructUtils.Merge(new List<object?> { ent, ctx2.Reqdata });
215
202
  }
216
203
  StructUtils.DelProp(ent, "$KEY");
217
204
  var outval = StructUtils.Clone(ent);
@@ -384,11 +371,16 @@ public class TestFeature : BaseFeature
384
371
  }
385
372
  }
386
373
 
387
- // Get required params.
388
- var paramsPath = StructUtils.GetPath(point, StructUtils.Jt("args", "params"));
389
- var reqdParams = StructUtils.Select(paramsPath,
390
- new Dictionary<string, object?> { ["reqd"] = true });
391
- var reqd = StructUtils.Transform(reqdParams,
374
+ // Path AND query: a path-only read misses a query-addressed record
375
+ // (e.g. GET /result?trace_id=), which has no path param at all.
376
+ var reqdArgs = new List<object?>();
377
+ foreach (var kind in new[] { "params", "query" })
378
+ {
379
+ var argsPath = StructUtils.GetPath(point, StructUtils.Jt("args", kind));
380
+ reqdArgs.AddRange(StructUtils.Select(argsPath,
381
+ new Dictionary<string, object?> { ["reqd"] = true }));
382
+ }
383
+ var reqd = StructUtils.Transform(reqdArgs,
392
384
  StructUtils.Jt("`$EACH`", "", "`$KEY.name`"));
393
385
 
394
386
  var qand = new List<object?>();
@@ -11,7 +11,19 @@ namespace ProjectNameSdk.Util;
11
11
 
12
12
  public static partial class SdkUtility
13
13
  {
14
- private static readonly HttpClient DefaultHttpClient = new();
14
+ // Cookies OFF on every handler. The clients are process-wide, so a
15
+ // CookieContainer would send one call's Set-Cookie on the next, under a
16
+ // different credential — and .NET ADDS container cookies to a request
17
+ // that already carries a Cookie header, which an `apiKey in: cookie`
18
+ // scheme does. Pooling is the handler's own and is unaffected.
19
+ private static HttpClientHandler CookielessHandler(bool allowRedirect) =>
20
+ new HttpClientHandler
21
+ {
22
+ AllowAutoRedirect = allowRedirect,
23
+ UseCookies = false,
24
+ };
25
+
26
+ private static readonly HttpClient DefaultHttpClient = new(CookielessHandler(true));
15
27
 
16
28
  // Non-following twin of DefaultHttpClient. The station feature's
17
29
  // middleware sets `redirect: manual` on the fetch definition under a
@@ -20,11 +32,8 @@ public static partial class SdkUtility
20
32
  // to a Location no policy approved (the ts donor's fetch honours the
21
33
  // same key natively; redirect policy is per-handler in .NET, hence a
22
34
  // second client).
23
- private static readonly HttpClient ManualRedirectHttpClient = new(
24
- new HttpClientHandler
25
- {
26
- AllowAutoRedirect = false,
27
- });
35
+ private static readonly HttpClient ManualRedirectHttpClient =
36
+ new(CookielessHandler(false));
28
37
 
29
38
  // Proxy-routed clients, cached per proxy URL and redirect policy (see
30
39
  // the proxy feature's fetchdef annotation).
@@ -43,12 +52,10 @@ public static partial class SdkUtility
43
52
  var key = (manual ? "manual|" : "auto|") + proxy;
44
53
  if (!ProxyClients.TryGetValue(key, out var client))
45
54
  {
46
- client = new HttpClient(new HttpClientHandler
47
- {
48
- Proxy = new System.Net.WebProxy(proxy),
49
- UseProxy = true,
50
- AllowAutoRedirect = !manual,
51
- });
55
+ var handler = CookielessHandler(!manual);
56
+ handler.Proxy = new System.Net.WebProxy(proxy);
57
+ handler.UseProxy = true;
58
+ client = new HttpClient(handler);
52
59
  ProxyClients[key] = client;
53
60
  }
54
61
  return client;
@@ -24,7 +24,7 @@ public static partial class SdkUtility
24
24
  {
25
25
  var key = item[0] as string ?? "";
26
26
  var val = item[1];
27
- if (val != null && !ContainsStr(paramnames, key))
27
+ if (val != null && "$action" != key && !ContainsStr(paramnames, key))
28
28
  {
29
29
  query[key] = val;
30
30
  }
@@ -20,13 +20,13 @@ public static partial class SdkUtility
20
20
  var transform = Helpers.ToMapAny(StructUtils.GetProp(point, "transform"));
21
21
  if (transform == null)
22
22
  {
23
- return ctx.Reqdata;
23
+ return StripAction(ctx.Reqdata);
24
24
  }
25
25
 
26
26
  var reqform = StructUtils.GetProp(transform, "req");
27
27
  if (reqform == null)
28
28
  {
29
- return ctx.Reqdata;
29
+ return StripAction(ctx.Reqdata);
30
30
  }
31
31
 
32
32
  var reqdata = StructUtils.Transform(new Dictionary<string, object?>
@@ -34,6 +34,26 @@ public static partial class SdkUtility
34
34
  ["reqdata"] = ctx.Reqdata,
35
35
  }, reqform);
36
36
 
37
- return reqdata;
37
+ return StripAction(reqdata);
38
+ }
39
+
40
+ // `$action` selects the point (see MakePointUtil); it is never an API
41
+ // field, so the body is a copy without it. The caller's map is left
42
+ // untouched.
43
+ private static object? StripAction(object? reqdata)
44
+ {
45
+ if (reqdata is not IDictionary<string, object?> src || !src.ContainsKey("$action"))
46
+ {
47
+ return reqdata;
48
+ }
49
+ var body = new Dictionary<string, object?>();
50
+ foreach (var kv in src)
51
+ {
52
+ if ("$action" != kv.Key)
53
+ {
54
+ body[kv.Key] = kv.Value;
55
+ }
56
+ }
57
+ return body;
38
58
  }
39
59
  }
@@ -57,7 +57,7 @@ defmodule ProjectName.Context do
57
57
  if S.ismap(pg), do: S.setprop(ctrl, "paging", pg)
58
58
  S.setprop(ctx, "ctrl", ctrl)
59
59
 
60
- basectx != nil and bp.("ctrl") != nil ->
60
+ basectx != nil and bp.("ctrl") != nil and gp.("opname") == nil ->
61
61
  S.setprop(ctx, "ctrl", bp.("ctrl"))
62
62
 
63
63
  true ->
@@ -4,8 +4,8 @@
4
4
  # stamps page/limit (or a cursor) into the request query; on the way back
5
5
  # (PreResult) it reads the server's pagination signals — a `Link: rel="next"`
6
6
  # header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
7
- # `next`/`cursor`/`nextCursor`/`hasMore` fields in the body — and records them
8
- # on `ctx.result.paging`. A per-call cursor/page supplied via ctrl paging
7
+ # `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case forms
8
+ # too) — and records them on `ctx.result.paging` and the caller's ctrl paging. A per-call cursor/page supplied via ctrl paging
9
9
  # takes priority (used by auto-iteration). Parameter names
10
10
  # (`pageParam`/`limitParam`/`cursorParam`), `startPage` and page size
11
11
  # (`limit`) are configurable.
@@ -67,7 +67,10 @@ defmodule ProjectName.Feature.Paging do
67
67
  S.setprop(query, cursor_param, S.getprop(paging, "cursor"))
68
68
 
69
69
  S.getprop(query, page_param) == nil ->
70
- page = S.getprop(paging, "page")
70
+ # A record written back by pre_result holds the page just fetched
71
+ # as "page" and the one to fetch as "nextPage", so nextPage wins.
72
+ page = S.getprop(paging, "nextPage")
73
+ page = if page == nil, do: S.getprop(paging, "page"), else: page
71
74
  page = if page == nil, do: por(S.getprop(opts, "startPage"), 1), else: page
72
75
  S.setprop(query, page_param, page)
73
76
 
@@ -182,18 +185,16 @@ defmodule ProjectName.Feature.Paging do
182
185
  bnext -> por(next_from_link, bnext)
183
186
  end
184
187
 
188
+ # snake_case forms rank below the camelCase ones.
185
189
  c =
186
190
  cond do
187
191
  S.getprop(body, "nextCursor") != nil -> S.getprop(body, "nextCursor")
188
192
  S.getprop(body, "cursor") != nil -> S.getprop(body, "cursor")
193
+ S.getprop(body, "next_cursor") != nil -> S.getprop(body, "next_cursor")
189
194
  true -> nil
190
195
  end
191
196
 
192
- hm =
193
- case S.getprop(body, "hasMore") do
194
- v when is_boolean(v) -> v
195
- _ -> false
196
- end
197
+ {hm, _} = body_more(body)
197
198
 
198
199
  {n, c, hm}
199
200
  else
@@ -239,10 +240,19 @@ defmodule ProjectName.Feature.Paging do
239
240
  # No usable relay `more` path, but a top-level body hasMore is
240
241
  # still an outright statement — keep it explicit, or cursor
241
242
  # inference below would flip an explicit false to true.
242
- {c, has_more, is_boolean(S.getprop(body, "hasMore"))}
243
+ {c, has_more, elem(body_more(body), 1)}
243
244
  end
244
245
  else
245
- {cursor_val, has_more, is_boolean(S.getprop(body, "hasMore"))}
246
+ {cursor_val, has_more, elem(body_more(body), 1)}
247
+ end
248
+
249
+ next_page =
250
+ if next_page == nil and S.ismap(body) do
251
+ np = S.getprop(body, "nextPage")
252
+ np = if np == nil, do: S.getprop(body, "next_page"), else: np
253
+ if is_number(np) or is_binary(np), do: np, else: nil
254
+ else
255
+ next_page
246
256
  end
247
257
 
248
258
  # Cursor presence only INFERS another page. When the server stated the
@@ -268,6 +278,7 @@ defmodule ProjectName.Feature.Paging do
268
278
  ])
269
279
 
270
280
  S.setprop(result, "paging", paging)
281
+ write_back(S.getprop(ctx, "ctrl"), paging)
271
282
  S.setprop(S.getprop(f, "client"), "_paging", S.jm(["last", paging]))
272
283
  end
273
284
  end
@@ -275,6 +286,37 @@ defmodule ProjectName.Feature.Paging do
275
286
  nil
276
287
  end
277
288
 
289
+ # The Context shares the caller's paging map by reference, so refill it in
290
+ # place; a fresh map on ctrl never reaches the caller.
291
+ defp write_back(ctrl, paging) do
292
+ shared = if ctrl == nil, do: nil, else: S.getprop(ctrl, "paging")
293
+
294
+ cond do
295
+ ctrl == nil ->
296
+ nil
297
+
298
+ S.ismap(shared) ->
299
+ Enum.each(S.keysof(shared), fn k -> S.delprop(shared, k) end)
300
+ Enum.each(S.keysof(paging), fn k -> S.setprop(shared, k, S.getprop(paging, k)) end)
301
+
302
+ true ->
303
+ S.setprop(ctrl, "paging", paging)
304
+ end
305
+
306
+ nil
307
+ end
308
+
309
+ # Body `hasMore`, or `has_more` when the camelCase form is absent: {value,
310
+ # stated outright}.
311
+ defp body_more(body) do
312
+ cond do
313
+ not S.ismap(body) -> {false, false}
314
+ is_boolean(S.getprop(body, "hasMore")) -> {S.getprop(body, "hasMore"), true}
315
+ is_boolean(S.getprop(body, "has_more")) -> {S.getprop(body, "has_more"), true}
316
+ true -> {false, false}
317
+ end
318
+ end
319
+
278
320
  defp is_list_op(f, ctx) do
279
321
  ops = opt_list(S.getprop(F.opts(f), "ops"), ["list"])
280
322
  op = S.getprop(ctx, "op")
@@ -167,20 +167,11 @@ defmodule ProjectName.Feature.Test do
167
167
  found = S.select(entmap, args)
168
168
  ent = S.getelem(found, 0)
169
169
 
170
- ent =
171
- if ent == nil and S.ismap(entmap) do
172
- ks = S.keysof(entmap)
173
- Enum.find_value(ks, fn k -> v = S.getprop(entmap, k); if S.ismap(v), do: v end)
174
- else
175
- ent
176
- end
177
-
178
170
  if ent == nil do
171
+ # update miss: 404, never another record
179
172
  respond(fctx, 404, nil, S.jm(["statusText", "Not found"]))
180
173
  else
181
- if S.ismap(ent) and reqdata != nil do
182
- Enum.each(H.entries(reqdata), fn {k, v} -> S.setprop(ent, k, v) end)
183
- end
174
+ if S.ismap(ent) and S.ismap(reqdata), do: S.merge(S.jt([ent, reqdata]))
184
175
 
185
176
  S.delprop(ent, "$KEY")
186
177
  respond(fctx, 200, S.clone(ent))
@@ -222,6 +213,45 @@ defmodule ProjectName.Feature.Test do
222
213
  :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
223
214
  end
224
215
 
216
+ defp point_terminal?(p) do
217
+ parts = S.getprop(p, "parts")
218
+ n = if S.islist(parts), do: S.size(parts), else: 0
219
+ last = if n > 0, do: S.getelem(parts, n - 1), else: nil
220
+ is_binary(last) and String.starts_with?(last, "{")
221
+ end
222
+
223
+ defp point_depth(p) do
224
+ parts = S.getprop(p, "parts")
225
+ if S.islist(parts), do: S.size(parts), else: 0
226
+ end
227
+
228
+ # The entity's own endpoint: a terminal `{param}` marks a record route,
229
+ # and among equals the shallower path wins (the same rule as make_point).
230
+ defp pick_point(points) do
231
+ n = if S.islist(points), do: S.size(points), else: 0
232
+
233
+ if n == 0 do
234
+ nil
235
+ else
236
+ rest = if n > 1, do: Enum.to_list(1..(n - 1)), else: []
237
+
238
+ Enum.reduce(rest, S.getelem(points, 0), fn i, point ->
239
+ cand = S.getelem(points, i)
240
+
241
+ cond do
242
+ point_terminal?(cand) != point_terminal?(point) -> if point_terminal?(cand), do: cand, else: point
243
+ point_depth(cand) < point_depth(point) -> cand
244
+ true -> point
245
+ end
246
+ end)
247
+ end
248
+ end
249
+
250
+ defp reqd_names(point, kind) do
251
+ reqd_args = S.select(S.getpath(point, "args." <> kind), S.jm(["reqd", true]))
252
+ S.transform(reqd_args, S.jt(["`$EACH`", "", "`$KEY.name`"]))
253
+ end
254
+
225
255
  # Build a struct select query ($AND of $OR alternatives) from the required
226
256
  # params + id, resolving each value through the pipeline param resolver.
227
257
  def build_args(_f, ctx, op, args) do
@@ -229,10 +259,11 @@ defmodule ProjectName.Feature.Test do
229
259
  config = S.getprop(ctx, "config")
230
260
  entname = Context.entity_name(S.getprop(ctx, "entity"))
231
261
  points = S.getpath(config, "entity." <> entname <> ".op." <> opname <> ".points")
232
- point = S.getelem(points, -1)
233
- params_path = S.getpath(point, "args.params")
234
- reqd_params = S.select(params_path, S.jm(["reqd", true]))
235
- reqd = S.transform(reqd_params, S.jt(["`$EACH`", "", "`$KEY.name`"]))
262
+ point = pick_point(points)
263
+ # Path AND query: a path-only read misses a query-addressed record
264
+ # (e.g. GET /result?trace_id=), which has no path param at all.
265
+ reqd_params = reqd_names(point, "params")
266
+ reqd_query = reqd_names(point, "query")
236
267
 
237
268
  qand = S.jt([])
238
269
  q = S.jm(["`$AND`", qand])
@@ -240,8 +271,7 @@ defmodule ProjectName.Feature.Test do
240
271
  if args != nil do
241
272
  Enum.each(S.keysof(args), fn key ->
242
273
  is_id = key == "id"
243
- selected = S.select(reqd, key)
244
- is_reqd = not S.isempty(selected)
274
+ is_reqd = not S.isempty(S.select(reqd_params, key)) or not S.isempty(S.select(reqd_query, key))
245
275
 
246
276
  if is_id or is_reqd do
247
277
  v = Utility.param(ctx, key)
@@ -1209,7 +1209,8 @@ defmodule ProjectName.Utility do
1209
1209
  out = S.jm([])
1210
1210
 
1211
1211
  Enum.each(H.entries(reqmatch), fn {key, val} ->
1212
- if val != nil and is_binary(key) and not Enum.member?(param_strs, key) do
1212
+ if val != nil and is_binary(key) and key != "$action" and
1213
+ not Enum.member?(param_strs, key) do
1213
1214
  S.setprop(out, key, val)
1214
1215
  end
1215
1216
  end)
@@ -1446,6 +1447,23 @@ defmodule ProjectName.Utility do
1446
1447
 
1447
1448
  # ---- transform_* ---------------------------------------------------------
1448
1449
 
1450
+ # `$action` selects the point (see make_point_impl); it is never an API
1451
+ # field, so the body is a copy without it. The caller's map is left
1452
+ # untouched.
1453
+ defp strip_action(reqdata) do
1454
+ if S.ismap(reqdata) and S.haskey(reqdata, "$action") do
1455
+ body = S.jm([])
1456
+
1457
+ Enum.each(H.entries(reqdata), fn {key, val} ->
1458
+ if key != "$action", do: S.setprop(body, key, val)
1459
+ end)
1460
+
1461
+ body
1462
+ else
1463
+ reqdata
1464
+ end
1465
+ end
1466
+
1449
1467
  def transform_request_impl(ctx) do
1450
1468
  spec = S.getprop(ctx, "spec")
1451
1469
  point = S.getprop(ctx, "point")
@@ -1453,17 +1471,20 @@ defmodule ProjectName.Utility do
1453
1471
 
1454
1472
  transform = H.to_map(S.getprop(point, "transform"))
1455
1473
 
1456
- if transform == nil do
1457
- S.getprop(ctx, "reqdata")
1458
- else
1459
- reqform = S.getprop(transform, "req")
1460
-
1461
- if reqform == nil do
1474
+ reqdata =
1475
+ if transform == nil do
1462
1476
  S.getprop(ctx, "reqdata")
1463
1477
  else
1464
- S.transform(S.jm(["reqdata", S.getprop(ctx, "reqdata")]), reqform)
1478
+ reqform = S.getprop(transform, "req")
1479
+
1480
+ if reqform == nil do
1481
+ S.getprop(ctx, "reqdata")
1482
+ else
1483
+ S.transform(S.jm(["reqdata", S.getprop(ctx, "reqdata")]), reqform)
1484
+ end
1465
1485
  end
1466
- end
1486
+
1487
+ strip_action(reqdata)
1467
1488
  end
1468
1489
 
1469
1490
  def transform_response_impl(ctx) do
@@ -9,18 +9,18 @@ import (
9
9
  )
10
10
 
11
11
  type Context struct {
12
- Id string
13
- Out map[string]any
14
- Ctrl *Control
15
- Meta map[string]any
16
- Client *ProjectNameSDK
17
- Utility *Utility
18
- Op *Operation
19
- Point map[string]any
20
- Config map[string]any
21
- Entopts map[string]any
22
- Options map[string]any
23
- Opmap map[string]*Operation
12
+ Id string
13
+ Out map[string]any
14
+ Ctrl *Control
15
+ Meta map[string]any
16
+ Client *ProjectNameSDK
17
+ Utility *Utility
18
+ Op *Operation
19
+ Point map[string]any
20
+ Config map[string]any
21
+ Entopts map[string]any
22
+ Options map[string]any
23
+ Opmap map[string]*Operation
24
24
  Opmu *sync.Mutex
25
25
  Response *Response
26
26
  Result *Result
@@ -86,7 +86,7 @@ func NewContext(ctxmap map[string]any, basectx *Context) *Context {
86
86
  } else if ctrl, ok := c.(*Control); ok {
87
87
  ctx.Ctrl = ctrl
88
88
  }
89
- } else if basectx != nil && basectx.Ctrl != nil {
89
+ } else if basectx != nil && basectx.Ctrl != nil && getCtxProp(ctxmap, "opname") == nil {
90
90
  ctx.Ctrl = basectx.Ctrl
91
91
  }
92
92
 
@@ -68,7 +68,11 @@ func (f *PagingFeature) PreRequest(ctx *core.Context) {
68
68
  if cursor, has := paging["cursor"]; has && cursor != nil {
69
69
  spec.Query[cursorParam] = cursor
70
70
  } else if spec.Query[pageParam] == nil {
71
- if page, has := paging["page"]; has && page != nil {
71
+ // A record written back by PreResult holds the page just fetched as
72
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
73
+ if next, has := paging["nextPage"]; has && next != nil {
74
+ spec.Query[pageParam] = next
75
+ } else if page, has := paging["page"]; has && page != nil {
72
76
  spec.Query[pageParam] = page
73
77
  } else {
74
78
  spec.Query[pageParam] = foptInt(f.options, "startPage", 1)
@@ -178,17 +182,36 @@ func (f *PagingFeature) PreResult(ctx *core.Context) {
178
182
  }
179
183
  }
180
184
 
181
- // Body-level cursors.
185
+ // Body-level signals; snake_case forms are read first so camelCase wins.
182
186
  if bm, ok := body.(map[string]any); ok {
183
187
  if bm["next"] != nil && paging["next"] == nil {
184
188
  paging["next"] = bm["next"]
185
189
  }
190
+ if bm["next_cursor"] != nil {
191
+ paging["cursor"] = bm["next_cursor"]
192
+ }
186
193
  if bm["cursor"] != nil {
187
194
  paging["cursor"] = bm["cursor"]
188
195
  }
189
196
  if bm["nextCursor"] != nil {
190
197
  paging["cursor"] = bm["nextCursor"]
191
198
  }
199
+ if paging["nextPage"] == nil {
200
+ np := bm["nextPage"]
201
+ if np == nil {
202
+ np = bm["next_page"]
203
+ }
204
+ switch n := np.(type) {
205
+ case float64:
206
+ paging["nextPage"] = int(n)
207
+ case int, string:
208
+ paging["nextPage"] = n
209
+ }
210
+ }
211
+ if hasMore, ok := bm["has_more"].(bool); ok {
212
+ paging["hasMore"] = hasMore
213
+ explicitMore = true
214
+ }
192
215
  if hasMore, ok := bm["hasMore"].(bool); ok {
193
216
  paging["hasMore"] = hasMore
194
217
  explicitMore = true
@@ -207,6 +230,21 @@ func (f *PagingFeature) PreResult(ctx *core.Context) {
207
230
 
208
231
  result.Paging = paging
209
232
  f.Last = paging
233
+
234
+ if ctx.Ctrl != nil {
235
+ // The Context shares the caller's paging map by reference, so refill
236
+ // it in place; a fresh map on Ctrl would never reach the caller.
237
+ if ctx.Ctrl.Paging != nil {
238
+ for k := range ctx.Ctrl.Paging {
239
+ delete(ctx.Ctrl.Paging, k)
240
+ }
241
+ for k, v := range paging {
242
+ ctx.Ctrl.Paging[k] = v
243
+ }
244
+ } else {
245
+ ctx.Ctrl.Paging = paging
246
+ }
247
+ }
210
248
  }
211
249
 
212
250
  func (f *PagingFeature) isList(ctx *core.Context) bool {