@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
@@ -3,7 +3,8 @@
3
3
  // query; on the way back (PreResult) it reads the server's pagination
4
4
  // signals — a `Link: rel="next"` header, `X-Page`/`X-Next-Page`/
5
5
  // `X-Total-Count` headers, or `next`/`cursor`/`nextCursor`/`hasMore` fields
6
- // in the body — and records them on `result.paging`. A per-call cursor/page
6
+ // in the body (snake_case forms too) — and records them on `result.paging`
7
+ // and the caller's ctrl paging. A per-call cursor/page
7
8
  // from ctrl takes priority (used by auto-iteration). Parameter names
8
9
  // (`pageParam`, `limitParam`, `cursorParam`), the page size (`limit`) and
9
10
  // the start page (`startPage`, default 1) are configurable.
@@ -225,7 +226,10 @@ static void paging_pre_request(PagingFeature* pf, Context* ctx) {
225
226
  if (!v_is_noval(cursor) && !v_is_null(cursor)) {
226
227
  setp(query, cursor_param, cursor);
227
228
  } else if (v_is_noval(getp(query, page_param))) {
228
- voxgig_value* page = getp(paging, "page");
229
+ // A record written back by pre_result holds the page just fetched as
230
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
231
+ voxgig_value* page = getp(paging, "nextPage");
232
+ if (v_is_noval(page) || v_is_null(page)) page = getp(paging, "page");
229
233
  if (!v_is_noval(page) && !v_is_null(page)) {
230
234
  setp(query, page_param, page);
231
235
  } else {
@@ -297,12 +301,16 @@ static void paging_pre_result(PagingFeature* pf, Context* ctx) {
297
301
  }
298
302
  }
299
303
 
300
- // Body-level cursors.
304
+ // Body-level signals; snake_case forms are read first so camelCase wins.
301
305
  if (voxgig_is_map(body)) {
302
306
  voxgig_value* next = getp(body, "next");
303
307
  if (!v_is_noval(next) && !v_is_null(next) && v_is_noval(getp(paging, "next"))) {
304
308
  setp(paging, "next", next);
305
309
  }
310
+ voxgig_value* snake_cursor = getp(body, "next_cursor");
311
+ if (!v_is_noval(snake_cursor) && !v_is_null(snake_cursor)) {
312
+ setp(paging, "cursor", snake_cursor);
313
+ }
306
314
  voxgig_value* cursor = getp(body, "cursor");
307
315
  if (!v_is_noval(cursor) && !v_is_null(cursor)) {
308
316
  setp(paging, "cursor", cursor);
@@ -311,7 +319,18 @@ static void paging_pre_result(PagingFeature* pf, Context* ctx) {
311
319
  if (!v_is_noval(next_cursor) && !v_is_null(next_cursor)) {
312
320
  setp(paging, "cursor", next_cursor);
313
321
  }
322
+ if (v_is_noval(getp(paging, "nextPage"))) {
323
+ voxgig_value* np = getp(body, "nextPage");
324
+ if (v_is_noval(np) || v_is_null(np)) np = getp(body, "next_page");
325
+ if (voxgig_is_number(np) || voxgig_is_string(np)) {
326
+ setp(paging, "nextPage", np);
327
+ }
328
+ }
314
329
  bool has_more;
330
+ if (get_bool(body, "has_more", &has_more)) {
331
+ setp(paging, "hasMore", v_bool(has_more));
332
+ explicit_more = true;
333
+ }
315
334
  if (get_bool(body, "hasMore", &has_more)) {
316
335
  setp(paging, "hasMore", v_bool(has_more));
317
336
  explicit_more = true;
@@ -334,6 +353,21 @@ static void paging_pre_result(PagingFeature* pf, Context* ctx) {
334
353
 
335
354
  ctx->result->paging = paging;
336
355
  pf->last = paging;
356
+
357
+ if (ctx->ctrl) {
358
+ // The Context shares the caller's paging map by pointer, so refill it in
359
+ // place; a fresh map on ctrl never reaches the caller.
360
+ if (voxgig_is_map(ctx->ctrl->paging)) {
361
+ voxgig_map* dst = voxgig_as_map(ctx->ctrl->paging);
362
+ voxgig_map* src = voxgig_as_map(paging);
363
+ voxgig_map_clear(dst);
364
+ for (size_t i = 0; i < voxgig_map_len(src); i++) {
365
+ voxgig_map_set(dst, voxgig_map_key_at(src, i), v_share(voxgig_map_val_at(src, i)));
366
+ }
367
+ } else {
368
+ ctx->ctrl->paging = paging;
369
+ }
370
+ }
337
371
  }
338
372
 
339
373
  static void paging_hook(Feature* f, const char* name, Context* ctx) {
@@ -92,23 +92,61 @@ static const char* test_entity_name(Context* ctx) {
92
92
  return ctx->op->entity;
93
93
  }
94
94
 
95
+ static bool point_terminal(voxgig_value* p) {
96
+ voxgig_value* parts = getp(p, "parts");
97
+ if (!voxgig_is_list(parts)) return false;
98
+ voxgig_list* l = voxgig_as_list(parts);
99
+ if (0 == l->len) return false;
100
+ voxgig_value* last = l->items[l->len - 1];
101
+ return voxgig_is_string(last) && '{' == voxgig_as_string(last)[0];
102
+ }
103
+
104
+ static size_t point_depth(voxgig_value* p) {
105
+ voxgig_value* parts = getp(p, "parts");
106
+ return voxgig_is_list(parts) ? voxgig_as_list(parts)->len : 0;
107
+ }
108
+
109
+ // The entity's own endpoint: a terminal `{param}` marks a record route, and
110
+ // among equals the shallower path wins (the same rule as make_point).
111
+ static voxgig_value* pick_point(voxgig_value* points) {
112
+ if (!voxgig_is_list(points)) return NULL;
113
+ voxgig_list* l = voxgig_as_list(points);
114
+ if (0 == l->len) return NULL;
115
+ voxgig_value* point = l->items[0];
116
+ for (size_t i = 1; i < l->len; i++) {
117
+ voxgig_value* cand = l->items[i];
118
+ if (point_terminal(cand) != point_terminal(point)) {
119
+ if (point_terminal(cand)) point = cand;
120
+ } else if (point_depth(cand) < point_depth(point)) {
121
+ point = cand;
122
+ }
123
+ }
124
+ return point;
125
+ }
126
+
127
+ static voxgig_value* reqd_names(voxgig_value* point, const char* kind) {
128
+ voxgig_value* args_path = getpath2(point, "args", kind);
129
+ voxgig_value* reqd_args = voxgig_select(args_path, cmap(1, "reqd", v_bool(true)));
130
+ voxgig_value* reqd_spec =
131
+ clist(3, v_str("`$EACH`"), v_str(""), v_str("`$KEY.name`"));
132
+ voxgig_value* reqd = voxgig_transform(reqd_args, reqd_spec, NULL);
133
+ if (!voxgig_is_list(reqd)) reqd = v_list();
134
+ return reqd;
135
+ }
136
+
95
137
  static voxgig_value* build_args(Context* ctx, voxgig_value* args) {
96
138
  Operation* op = ctx->op;
97
139
  const char* opname = op->name;
98
140
  const char* entname = test_entity_name(ctx);
99
141
 
100
- // Get last point from config.
101
142
  const char* ppath[6] = {"entity", entname, "op", opname, "points", NULL};
102
143
  voxgig_value* points = getpath_c(ctx->config, ppath);
103
- voxgig_value* point = voxgig_getelem(points, v_int(-1), NULL);
144
+ voxgig_value* point = pick_point(points);
104
145
 
105
- // Get required params.
106
- voxgig_value* params_path = getpath2(point, "args", "params");
107
- voxgig_value* reqd_params = voxgig_select(params_path, cmap(1, "reqd", v_bool(true)));
108
- voxgig_value* reqd_spec =
109
- clist(3, v_str("`$EACH`"), v_str(""), v_str("`$KEY.name`"));
110
- voxgig_value* reqd = voxgig_transform(reqd_params, reqd_spec, NULL);
111
- if (!voxgig_is_list(reqd)) reqd = v_list();
146
+ // Path AND query: a path-only read misses a query-addressed record
147
+ // (e.g. GET /result?trace_id=), which has no path param at all.
148
+ voxgig_value* reqd_params = reqd_names(point, "params");
149
+ voxgig_value* reqd_query = reqd_names(point, "query");
112
150
 
113
151
  voxgig_value* qand = v_list();
114
152
  voxgig_value* q = cmap(1, "`$AND`", qand);
@@ -118,8 +156,8 @@ static voxgig_value* build_args(Context* ctx, voxgig_value* args) {
118
156
  for (size_t ki = 0; ki < keys.len; ki++) {
119
157
  const char* key = keys.data[ki];
120
158
  bool is_id = strcmp(key, "id") == 0;
121
- voxgig_value* selected = voxgig_select(reqd, v_str(key));
122
- bool is_reqd = !voxgig_isempty(selected);
159
+ bool is_reqd = !voxgig_isempty(voxgig_select(reqd_params, v_str(key))) ||
160
+ !voxgig_isempty(voxgig_select(reqd_query, v_str(key)));
123
161
 
124
162
  if (is_id || is_reqd) {
125
163
  voxgig_value* v = param_util(ctx, v_str(key));
@@ -209,28 +247,14 @@ static voxgig_value* test_fetch(voxgig_value* entity, Context* ctx, const char*
209
247
  voxgig_value* args = build_args(ctx, update_match);
210
248
  voxgig_value* found = voxgig_select(entmap, args);
211
249
  voxgig_value* ent = voxgig_getelem(found, v_int(0), NULL);
212
- if ((v_is_noval(ent) || v_is_null(ent)) && voxgig_size(entmap) > 0) {
213
- // Fall back to any entity in the fixture.
214
- voxgig_value* items = voxgig_items_v(entmap);
215
- voxgig_list* il = voxgig_as_list(items);
216
- for (size_t i = 0; i < il->len; i++) {
217
- voxgig_value* e = voxgig_getelem(il->items[i], v_int(1), NULL);
218
- if (voxgig_is_map(e)) {
219
- ent = e;
220
- break;
221
- }
222
- }
223
- }
224
250
  if (v_is_noval(ent) || v_is_null(ent)) {
251
+ // update miss: 404, never another record
225
252
  voxgig_value* r = respond(ctx, 404, v_undef());
226
253
  setp(r, "statusText", v_str("Not found"));
227
254
  return r;
228
255
  }
229
256
  if (voxgig_is_map(ent) && voxgig_is_map(reqdata)) {
230
- voxgig_map* rm = voxgig_as_map(reqdata);
231
- for (size_t i = 0; i < rm->len; i++) {
232
- setp(ent, rm->entries[i].key, rm->entries[i].value);
233
- }
257
+ ent = voxgig_merge(clist(2, v_share(ent), v_share(reqdata)), VOXGIG_MAXDEPTH);
234
258
  }
235
259
  voxgig_delprop(ent, v_str("$KEY"));
236
260
  return respond(ctx, 200, voxgig_clone(ent));
@@ -28,7 +28,8 @@ voxgig_value* prepare_query_util(Context* ctx) {
28
28
  for (size_t i = 0; i < rm->len; i++) {
29
29
  const char* key = rm->entries[i].key;
30
30
  voxgig_value* val = rm->entries[i].value;
31
- if (!v_is_noval(val) && !v_is_null(val) && !params_contains(params, key)) {
31
+ if (!v_is_noval(val) && !v_is_null(val) && strcmp(key, "$action") != 0 &&
32
+ !params_contains(params, key)) {
32
33
  setp(out, key, v_share(val));
33
34
  }
34
35
  }
@@ -2,6 +2,28 @@
2
2
 
3
3
  #include "sdk.h"
4
4
 
5
+ #include <string.h>
6
+
7
+ /* `$action` selects the point (see make_point_util); it is never an API
8
+ field, so the body is a copy without it. The caller's map is left
9
+ untouched. */
10
+ static voxgig_value* strip_action(voxgig_value* reqdata) {
11
+ if (!voxgig_is_map(reqdata)) return reqdata;
12
+ voxgig_map* rm = voxgig_as_map(reqdata);
13
+ bool has = false;
14
+ for (size_t i = 0; i < rm->len; i++) {
15
+ if (strcmp(rm->entries[i].key, "$action") == 0) { has = true; break; }
16
+ }
17
+ if (!has) return reqdata;
18
+ voxgig_value* body = voxgig_new_map();
19
+ for (size_t i = 0; i < rm->len; i++) {
20
+ if (strcmp(rm->entries[i].key, "$action") != 0) {
21
+ setp(body, rm->entries[i].key, v_share(rm->entries[i].value));
22
+ }
23
+ }
24
+ return body;
25
+ }
26
+
5
27
  voxgig_value* transform_request_util(Context* ctx) {
6
28
  Spec* spec = ctx->spec;
7
29
  voxgig_value* point = ctx->point;
@@ -9,14 +31,14 @@ voxgig_value* transform_request_util(Context* ctx) {
9
31
  if (spec) spec_set_step(spec, "reqform");
10
32
 
11
33
  voxgig_value* transform = to_map(getp(point, "transform"));
12
- if (v_is_noval(transform)) return ctx->reqdata;
34
+ if (v_is_noval(transform)) return strip_action(ctx->reqdata);
13
35
 
14
36
  voxgig_value* reqform = getp(transform, "req");
15
- if (v_is_noval(reqform) || v_is_null(reqform)) return ctx->reqdata;
37
+ if (v_is_noval(reqform) || v_is_null(reqform)) return strip_action(ctx->reqdata);
16
38
 
17
39
  voxgig_value* store = cmap(1, "reqdata", v_share(ctx->reqdata));
18
40
  /* Return the transform result verbatim, as the ts reference does. Falling
19
41
  back to ctx->reqdata meant a reqform naming a path that does not resolve
20
42
  sent the WHOLE request data instead of nothing. */
21
- return voxgig_transform(store, reqform, NULL);
43
+ return strip_action(voxgig_transform(store, reqform, NULL));
22
44
  }
@@ -305,7 +305,9 @@
305
305
  cprop (fn [k] (mget ctxmap k))
306
306
  client (or (cprop "client") (bget :client))
307
307
  utility (or (cprop "utility") (bget :utility))
308
- ;; ctrl: explicit ctrl map -> fresh control; else share base ctrl.
308
+ ;; ctrl: explicit ctrl map -> fresh control; an OPERATION (one named by
309
+ ;; opname) also gets its own, so a paging cursor cannot leak into the
310
+ ;; next call; anything else shares the base ctrl.
309
311
  ctrl-raw (cprop "ctrl")
310
312
  ctrl (cond
311
313
  (vs/ismap ctrl-raw)
@@ -315,7 +317,7 @@
315
317
  (when (contains? (into #{} (vs/keysof ctrl-raw)) "actor") (oset! c :actor (mget ctrl-raw "actor")))
316
318
  (when (vs/ismap (mget ctrl-raw "paging")) (oset! c :paging (mget ctrl-raw "paging")))
317
319
  c)
318
- (bget :ctrl) (bget :ctrl)
320
+ (and (nil? (cprop "opname")) (bget :ctrl)) (bget :ctrl)
319
321
  :else (make-control))
320
322
  meta (let [m (cprop "meta")] (if (vs/ismap m) m (or (bget :meta) (vs/jm))))
321
323
  config (let [c (cprop "config")] (if (vs/ismap c) c (bget :config)))
@@ -502,18 +504,30 @@
502
504
  out (vs/jm)]
503
505
  (doseq [item (or (vs/items reqmatch) [])]
504
506
  (let [k (vs/getprop item 0) v (vs/getprop item 1)]
505
- (when (and (some? v) (string? k) (not (contains? pset k)))
507
+ (when (and (some? v) (string? k) (not= "$action" k) (not (contains? pset k)))
506
508
  (.put ^java.util.Map out k v))))
507
509
  out))
508
510
 
511
+ ;; `$action` selects the point (see u-make-point); it is never an API field,
512
+ ;; so the body is a copy without it. The caller's map is left untouched.
513
+ (defn- strip-action [reqdata]
514
+ (if (and (vs/ismap reqdata) (.containsKey ^java.util.Map reqdata "$action"))
515
+ (let [body (vs/jm)]
516
+ (doseq [item (or (vs/items reqdata) [])]
517
+ (let [k (vs/getprop item 0) v (vs/getprop item 1)]
518
+ (when (not= "$action" k) (.put ^java.util.Map body k v))))
519
+ body)
520
+ reqdata))
521
+
509
522
  (defn u-transform-request [ctx]
510
523
  (let [spec (oget ctx :spec) point (oget ctx :point)]
511
524
  (when spec (oset! spec :step "reqform"))
512
525
  (let [transform (to-map (vs/getprop point "transform"))]
513
- (if (nil? transform) (oget ctx :reqdata)
514
- (let [reqform (vs/getprop transform "req")]
515
- (if (nil? reqform) (oget ctx :reqdata)
516
- (vs/transform (vs/jm "reqdata" (oget ctx :reqdata)) reqform)))))))
526
+ (strip-action
527
+ (if (nil? transform) (oget ctx :reqdata)
528
+ (let [reqform (vs/getprop transform "req")]
529
+ (if (nil? reqform) (oget ctx :reqdata)
530
+ (vs/transform (vs/jm "reqdata" (oget ctx :reqdata)) reqform))))))))
517
531
 
518
532
  (defn u-prepare-body [ctx]
519
533
  (if (= "data" (op-input (oget ctx :op))) (ucall ctx :transform-request) nil))
@@ -177,14 +177,11 @@
177
177
  (let [update-match (if (zero? (vs/size update-match)) (resolve-match (vs/jm)) update-match)
178
178
  args (test-build-args fctx op update-match)
179
179
  found (vs/select entmap args)
180
- ent0 (vs/getelem found 0)
181
- ent (if (and (nil? ent0) (vs/ismap entmap) (pos? (vs/size entmap)))
182
- (first (filter vs/ismap (map #(vs/getprop entmap %) (vs/keysof entmap))))
183
- ent0)]
180
+ ent (vs/getelem found 0)]
181
+ ;; update miss: 404, never another record
184
182
  (if (nil? ent) (respond 404 nil (vs/jm "statusText" "Not found"))
185
- (do (when (and (vs/ismap ent) reqdata)
186
- (doseq [item (or (vs/items reqdata) [])]
187
- (.put ^java.util.Map ent (vs/getprop item 0) (vs/getprop item 1))))
183
+ (do (when (and (vs/ismap ent) (vs/ismap reqdata))
184
+ (vs/merge (vs/jt ent reqdata)))
188
185
  (vs/delprop ent "$KEY")
189
186
  (respond 200 (vs/clone ent) nil)))))
190
187
  (= opn "remove")
@@ -210,20 +207,47 @@
210
207
  (if (nil? net) test-fetcher (make-netsim fa net test-fetcher)))))))
211
208
  fa))
212
209
 
210
+ (defn- point-terminal? [p]
211
+ (let [parts (vs/getprop p "parts")
212
+ n (if (vs/islist parts) (vs/size parts) 0)
213
+ last-part (when (pos? n) (vs/getelem parts (dec n)))]
214
+ (and (string? last-part) (str/starts-with? last-part "{"))))
215
+
216
+ (defn- point-depth [p]
217
+ (let [parts (vs/getprop p "parts")]
218
+ (if (vs/islist parts) (vs/size parts) 0)))
219
+
220
+ ;; The entity's own endpoint: a terminal `{param}` marks a record route, and
221
+ ;; among equals the shallower path wins (the same rule as make-point).
222
+ (defn- pick-point [points]
223
+ (when (and (vs/islist points) (pos? (vs/size points)))
224
+ (reduce (fn [point cand]
225
+ (cond
226
+ (not= (point-terminal? cand) (point-terminal? point)) (if (point-terminal? cand) cand point)
227
+ (< (point-depth cand) (point-depth point)) cand
228
+ :else point))
229
+ (vs/getelem points 0)
230
+ (map #(vs/getelem points %) (range 1 (vs/size points))))))
231
+
232
+ (defn- reqd-names [point kind]
233
+ (vs/transform (vs/select (vs/getpath point (str "args." kind)) (vs/jm "reqd" true))
234
+ (vs/jt "`$EACH`" "" "`$KEY.name`")))
235
+
213
236
  (defn- test-build-args [ctx op args]
214
237
  (let [opname (core/op-name op)
215
238
  points (vs/getpath (core/oget ctx :config)
216
239
  (str "entity." (core/entity-get-name (core/oget ctx :entity)) ".op." opname ".points"))
217
- point (vs/getelem points -1)
218
- params-path (vs/getpath point "args.params")
219
- reqd-params (vs/select params-path (vs/jm "reqd" true))
220
- reqd (vs/transform reqd-params (vs/jt "`$EACH`" "" "`$KEY.name`"))
240
+ point (pick-point points)
241
+ ;; Path AND query: a path-only read misses a query-addressed record
242
+ ;; (e.g. GET /result?trace_id=), which has no path param at all.
243
+ reqd-params (reqd-names point "params")
244
+ reqd-query (reqd-names point "query")
221
245
  qand (vs/jt)]
222
246
  (when args
223
247
  (doseq [key (or (vs/keysof args) [])]
224
248
  (let [is-id (= key "id")
225
- selected (vs/select reqd key)
226
- is-reqd (not (vs/isempty selected))]
249
+ is-reqd (or (not (vs/isempty (vs/select reqd-params key)))
250
+ (not (vs/isempty (vs/select reqd-query key))))]
227
251
  (when (or is-id is-reqd)
228
252
  (let [v ((core/uget ctx :param) ctx key)
229
253
  ka (when (core/op-alias op) (vs/getprop (core/op-alias op) key))
@@ -564,7 +588,14 @@
564
588
  (cond
565
589
  (some? (mget paging "cursor")) (.put ^java.util.Map q cursor-param (mget paging "cursor"))
566
590
  (nil? (mget q page-param))
567
- (.put ^java.util.Map q page-param (if (nil? (mget paging "page")) (opt fa "startPage" 1) (mget paging "page"))))
591
+ ;; A record written back by PreResult holds the page
592
+ ;; just fetched as `page` and the one to fetch as
593
+ ;; `nextPage`, so nextPage wins.
594
+ (.put ^java.util.Map q page-param
595
+ (cond
596
+ (some? (mget paging "nextPage")) (mget paging "nextPage")
597
+ (some? (mget paging "page")) (mget paging "page")
598
+ :else (opt fa "startPage" 1))))
568
599
  (when (and (some? (opt fa "limit")) (nil? (mget q limit-param)))
569
600
  (.put ^java.util.Map q limit-param (opt fa "limit"))))))))))
570
601
  "PreResult"
@@ -609,12 +640,21 @@
609
640
  (reset! explicit-more true))))))
610
641
  (when (vs/ismap body)
611
642
  (when (some? (mget body "next")) (when (nil? (mget paging "next")) (.put ^java.util.Map paging "next" (mget body "next"))))
643
+ ;; Both spellings, camelCase last so it wins when a
644
+ ;; body carries the two.
645
+ (when (some? (mget body "next_cursor")) (.put ^java.util.Map paging "cursor" (mget body "next_cursor")))
612
646
  (when (some? (mget body "cursor")) (.put ^java.util.Map paging "cursor" (mget body "cursor")))
613
647
  (when (some? (mget body "nextCursor")) (.put ^java.util.Map paging "cursor" (mget body "nextCursor")))
614
- (let [hm (mget body "hasMore")]
615
- (when (or (= hm true) (= hm false))
616
- (.put ^java.util.Map paging "hasMore" hm)
617
- (reset! explicit-more true))))
648
+ (when (nil? (mget paging "nextPage"))
649
+ (let [np (if (some? (mget body "nextPage"))
650
+ (mget body "nextPage") (mget body "next_page"))]
651
+ (when (or (number? np) (string? np))
652
+ (.put ^java.util.Map paging "nextPage" np))))
653
+ (doseq [k ["has_more" "hasMore"]]
654
+ (let [hm (mget body k)]
655
+ (when (or (= hm true) (= hm false))
656
+ (.put ^java.util.Map paging "hasMore" hm)
657
+ (reset! explicit-more true)))))
618
658
  ;; Cursor presence only INFERS another page. When the
619
659
  ;; server stated the answer outright — relay's
620
660
  ;; `hasNextPage: false`, or a body `hasMore` — that
@@ -627,6 +667,16 @@
627
667
  (boolean (or (mget paging "hasMore") (some? (mget paging "next"))
628
668
  (some? (mget paging "cursor")) (some? (mget paging "nextPage")))))))
629
669
  (core/oset! result :paging paging)
670
+ (when-let [ctrl (core/oget ctx :ctrl)]
671
+ (let [held (core/oget ctrl :paging)]
672
+ (if (vs/ismap held)
673
+ ;; The caller holds this map, so refill it in place
674
+ ;; as the other typed ports do; a fresh map on ctrl
675
+ ;; would never reach them. Every key of the record
676
+ ;; is written, so no key of an earlier page lives on.
677
+ (doseq [k (vec (vs/keysof paging))]
678
+ (.put ^java.util.Map held k (mget paging k)))
679
+ (core/oset! ctrl :paging paging))))
630
680
  (track-set! fa "_paging" (vs/jm "last" paging)))))))
631
681
  )
632
682
  fa))
@@ -690,7 +690,7 @@ inline Context::Context(const CtxSpec& cs, const CtxPtr& basectx) {
690
690
  if (a.is_string()) ctrl->actor = a.as_string();
691
691
  Value p = getp(cs.ctrlMap, "paging");
692
692
  if (p.is_map()) ctrl->paging = p;
693
- } else if (basectx && basectx->ctrl) {
693
+ } else if (basectx && basectx->ctrl && cs.opname.empty()) {
694
694
  ctrl = basectx->ctrl;
695
695
  }
696
696
 
@@ -3,7 +3,8 @@
3
3
  // page/limit (or a cursor) into the request query; on the way back
4
4
  // (PreResult) it reads the server's pagination signals — a `Link` rel="next"
5
5
  // header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
6
- // `next`/`cursor`/`nextCursor`/`hasMore` body fields — onto ctx.result.paging.
6
+ // `next`/`cursor`/`nextCursor`/`hasMore` body fields (snake_case forms too)
7
+ // — onto ctx.result.paging and the caller's ctrl paging.
7
8
  // A per-call cursor/page from ctrl takes priority (auto-iteration).
8
9
 
9
10
  #ifndef SDK_FEATURE_PAGING_HPP
@@ -61,7 +62,10 @@ public:
61
62
  if (!is_nullish(cursor)) {
62
63
  map_put(spec->query, cursorParam, cursor);
63
64
  } else if (is_nullish(getp(spec->query, pageParam))) {
64
- Value page = getp(paging, "page");
65
+ // A record written back by preResult holds the page just fetched as
66
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
67
+ Value page = getp(paging, "nextPage");
68
+ if (is_nullish(page)) page = getp(paging, "page");
65
69
  if (!is_nullish(page)) {
66
70
  map_put(spec->query, pageParam, page);
67
71
  } else {
@@ -171,12 +175,16 @@ public:
171
175
  }
172
176
  }
173
177
 
174
- // Body-level cursors.
178
+ // Body-level signals; snake_case forms are read first so camelCase wins.
175
179
  if (body.is_map()) {
176
180
  Value bnext = getp(body, "next");
177
181
  if (!is_nullish(bnext) && is_nullish(getp(paging, "next"))) {
178
182
  map_put(paging, "next", bnext);
179
183
  }
184
+ Value bsnakeCursor = getp(body, "next_cursor");
185
+ if (!is_nullish(bsnakeCursor)) {
186
+ map_put(paging, "cursor", bsnakeCursor);
187
+ }
180
188
  Value bcursor = getp(body, "cursor");
181
189
  if (!is_nullish(bcursor)) {
182
190
  map_put(paging, "cursor", bcursor);
@@ -185,6 +193,18 @@ public:
185
193
  if (!is_nullish(bnextCursor)) {
186
194
  map_put(paging, "cursor", bnextCursor);
187
195
  }
196
+ if (is_nullish(getp(paging, "nextPage"))) {
197
+ Value np = getp(body, "nextPage");
198
+ if (is_nullish(np)) np = getp(body, "next_page");
199
+ if (np.is_number() || np.is_string()) {
200
+ map_put(paging, "nextPage", np);
201
+ }
202
+ }
203
+ Value bsnakeMore = getp(body, "has_more");
204
+ if (bsnakeMore.is_bool()) {
205
+ map_put(paging, "hasMore", bsnakeMore);
206
+ explicitMore = true;
207
+ }
188
208
  Value bhasMore = getp(body, "hasMore");
189
209
  if (bhasMore.is_bool()) {
190
210
  map_put(paging, "hasMore", bhasMore);
@@ -205,6 +225,19 @@ public:
205
225
 
206
226
  result->paging = paging;
207
227
  last = paging;
228
+
229
+ if (ctx->ctrl) {
230
+ // Value maps are shared_ptr-backed, so the caller's map is refilled in
231
+ // place; a fresh map on ctrl never reaches the caller.
232
+ if (ctx->ctrl->paging.is_map()) {
233
+ ctx->ctrl->paging.as_map()->clear();
234
+ for (const auto& kv : *paging.as_map()) {
235
+ map_put(ctx->ctrl->paging, kv.first, kv.second);
236
+ }
237
+ } else {
238
+ ctx->ctrl->paging = paging;
239
+ }
240
+ }
208
241
  }
209
242
 
210
243
  private:
@@ -170,14 +170,10 @@ private:
170
170
  Value args = buildArgs(ctx, op, updateMatch);
171
171
  std::vector<Value> found = Struct::select(entmap, args);
172
172
  Value ent = found.empty() ? Value::undef() : found[0];
173
- if (is_nullish(ent) && entmap.is_map()) {
174
- for (const auto& kv : *entmap.as_map()) {
175
- if (kv.second.is_map()) { ent = kv.second; break; }
176
- }
177
- }
173
+ // update miss: 404, never another record
178
174
  if (is_nullish(ent)) return respond(ctx, 404, Value(nullptr), extra1("statusText", Value("Not found")));
179
175
  if (ent.is_map() && ctx->reqdata.is_map()) {
180
- for (const auto& kv : *ctx->reqdata.as_map()) map_put(ent, kv.first, kv.second);
176
+ Struct::merge(vlist({ent, ctx->reqdata}));
181
177
  }
182
178
  Struct::delprop(ent, Value("$KEY"));
183
179
  Value out = Struct::clone(ent);
@@ -272,19 +268,51 @@ private:
272
268
  fopt::foptSleep(net)(ms);
273
269
  }
274
270
 
271
+ static bool pointTerminal(const Value& p) {
272
+ Value parts = getp(p, "parts");
273
+ if (!parts.is_list() || parts.as_list()->empty()) return false;
274
+ const Value& last = parts.as_list()->back();
275
+ return last.is_string() && 0 == last.as_string().rfind("{", 0);
276
+ }
277
+
278
+ static size_t pointDepth(const Value& p) {
279
+ Value parts = getp(p, "parts");
280
+ return parts.is_list() ? parts.as_list()->size() : 0;
281
+ }
282
+
283
+ // The entity's own endpoint: a terminal `{param}` marks a record route, and
284
+ // among equals the shallower path wins (the same rule as makePoint).
285
+ static Value pickPoint(const Value& points) {
286
+ if (!points.is_list() || points.as_list()->empty()) return Value::undef();
287
+ Value point = (*points.as_list())[0];
288
+ for (const Value& cand : *points.as_list()) {
289
+ if (pointTerminal(cand) != pointTerminal(point)) {
290
+ if (pointTerminal(cand)) point = cand;
291
+ } else if (pointDepth(cand) < pointDepth(point)) {
292
+ point = cand;
293
+ }
294
+ }
295
+ return point;
296
+ }
297
+
275
298
  Value buildArgs(CtxPtr ctx, OperationPtr op, const Value& args) {
276
299
  std::string opname = op->name;
277
300
 
278
301
  Value points = Struct::getpath(ctx->config,
279
302
  {"entity", ctx->entity == nullptr ? std::string("") : ctx->entity->getName(),
280
303
  "op", opname, "points"});
281
- Value point = Struct::getelem(points, -1);
282
-
283
- Value paramsPath = Struct::getpath(point, {"args", "params"});
284
- std::vector<Value> reqdParamsSel = Struct::select(paramsPath, vmap({{"reqd", Value(true)}}));
285
- Value reqdParams = vlist();
286
- for (auto& p : reqdParamsSel) reqdParams.as_list()->push_back(p);
287
- Value reqd = Struct::transform(reqdParams, vlist({Value("`$EACH`"), Value(""), Value("`$KEY.name`")}));
304
+ Value point = pickPoint(points);
305
+
306
+ // Path AND query: a path-only read misses a query-addressed record
307
+ // (e.g. GET /result?trace_id=), which has no path param at all.
308
+ Value reqdArgs = vlist();
309
+ for (const char* kind : {"params", "query"}) {
310
+ Value argsPath = Struct::getpath(point, {"args", kind});
311
+ for (auto& p : Struct::select(argsPath, vmap({{"reqd", Value(true)}}))) {
312
+ reqdArgs.as_list()->push_back(p);
313
+ }
314
+ }
315
+ Value reqd = Struct::transform(reqdArgs, vlist({Value("`$EACH`"), Value(""), Value("`$KEY.name`")}));
288
316
 
289
317
  Value qand = vlist();
290
318
  Value q = vmap({{"`$AND`", qand}});
@@ -1026,7 +1026,7 @@ inline Value prepareQuery(CtxPtr ctx) {
1026
1026
  for (const auto& item : Struct::items(reqmatch)) {
1027
1027
  std::string key = as_str(pair_key(item));
1028
1028
  Value val = pair_val(item);
1029
- if (!is_nullish(val) && !contains_str(params, key)) {
1029
+ if (!is_nullish(val) && "$action" != key && !contains_str(params, key)) {
1030
1030
  map_put(out, key, val);
1031
1031
  }
1032
1032
  }
@@ -1050,18 +1050,30 @@ inline std::string preparePath(CtxPtr ctx) {
1050
1050
 
1051
1051
  // ---- transformRequest -------------------------------------------------
1052
1052
 
1053
+ // `$action` selects the point (see makePoint); it is never an API field, so
1054
+ // the body is a copy without it. The caller's map is left untouched.
1055
+ inline Value stripAction(const Value& reqdata) {
1056
+ if (!map_contains(reqdata, "$action")) return reqdata;
1057
+ Value body = vmap();
1058
+ for (const auto& item : Struct::items(reqdata)) {
1059
+ std::string key = as_str(pair_key(item));
1060
+ if ("$action" != key) map_put(body, key, pair_val(item));
1061
+ }
1062
+ return body;
1063
+ }
1064
+
1053
1065
  inline Value transformRequest(CtxPtr ctx) {
1054
1066
  if (ctx->spec) ctx->spec->step = "reqform";
1055
1067
 
1056
1068
  Value transform = Helpers::toMapAny(getp(ctx->point, "transform"));
1057
- if (!transform.is_map()) return ctx->reqdata;
1069
+ if (!transform.is_map()) return stripAction(ctx->reqdata);
1058
1070
 
1059
1071
  Value reqform = getp(transform, "req");
1060
- if (is_nullish(reqform)) return ctx->reqdata;
1072
+ if (is_nullish(reqform)) return stripAction(ctx->reqdata);
1061
1073
 
1062
1074
  Value data = vmap();
1063
1075
  map_put(data, "reqdata", ctx->reqdata);
1064
- return Struct::transform(data, reqform);
1076
+ return stripAction(Struct::transform(data, reqform));
1065
1077
  }
1066
1078
 
1067
1079
  // ---- makeOptions ------------------------------------------------------
@@ -78,7 +78,7 @@ public class Context
78
78
  {
79
79
  Ctrl = ctrl;
80
80
  }
81
- else if (basectx?.Ctrl != null)
81
+ else if (basectx?.Ctrl != null && Helpers.GetCtxProp(ctxmap, "opname") == null)
82
82
  {
83
83
  Ctrl = basectx.Ctrl;
84
84
  }