@voxgig/sdkgen 4.23.0 → 4.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) 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/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -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 — and records them on
11
- // `ctx.result.paging`. Generated SDKs build auto-iteration on top of this
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
- spec.query[pageParam] = null != paging.page ? paging.page : (this._options.startPage || 1)
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 cursors.
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
  }
@@ -148,6 +148,7 @@ class TestFeature extends BaseFeature {
148
148
  const found = select(entmap, args)
149
149
  const ent = getelem(found, 0)
150
150
  if (null == ent) {
151
+ // update miss: 404, never another record
151
152
  return respond(404, undefined, { statusText: S_NOT_FOUND })
152
153
  }
153
154
  else {
@@ -161,13 +162,12 @@ class TestFeature extends BaseFeature {
161
162
  const args = self.buildArgs(ctx, op, ctx.reqmatch)
162
163
  const found = select(entmap, args)
163
164
  const ent = getelem(found, 0)
164
- if (null == ent) {
165
- return respond(404, undefined, { statusText: S_NOT_FOUND })
166
- }
167
- else {
165
+ // Remove only the first matched entity. If nothing matches,
166
+ // succeed as a no-op rather than erroring.
167
+ if (null != ent) {
168
168
  delprop(entmap, getprop(ent, 'id'))
169
- return respond(200)
170
169
  }
170
+ return respond(200)
171
171
  }
172
172
  else if ('create' === op.name) {
173
173
  const args = self.buildArgs(ctx, op, ctx.reqdata)
@@ -183,6 +183,8 @@ class TestFeature extends BaseFeature {
183
183
  const out = clone(ent)
184
184
  return respond(200, out)
185
185
  }
186
+
187
+ return respond(404, undefined, { statusText: 'Unknown operation' })
186
188
  }
187
189
 
188
190
  // Optional network behaviour simulation over the mock transport. Enable
@@ -13,7 +13,7 @@ function prepareQuery(ctx) {
13
13
 
14
14
  const out = {}
15
15
  for (let [key, val] of items(reqmatch)) {
16
- if (null != val && !params.includes(key)) {
16
+ if (null != val && '$action' !== key && !params.includes(key)) {
17
17
  out[key] = val
18
18
  }
19
19
  }
@@ -254,6 +254,10 @@ describe('FeatureCorpus', () => {
254
254
 
255
255
  const probe = makeClient({})
256
256
  if (!probe._rootctx.config.hasFeature(name)) {
257
+ // The one line every runner prints for a section that names a
258
+ // feature this SDK does not generate, so an inert section is
259
+ // visible in the output rather than inferred from a green tick.
260
+ t.diagnostic(`feature.${name}: inert (this SDK does not generate the feature)`)
257
261
  return t.skip(`this SDK was generated without the ${name} feature`)
258
262
  }
259
263
 
@@ -323,45 +323,111 @@ describe('pipeline:feature order', () => {
323
323
  })
324
324
 
325
325
 
326
+ // A cookie credential as prepareAuth writes it: `<scheme>=K` for the probe
327
+ // key, with no scheme prefix and nothing else in the bag.
328
+ const COOKIE_PAIR = /^[^=;]+=K$/
329
+
330
+
326
331
  describe('pipeline:prepareAuth', () => {
327
332
 
328
333
  // Fake client so the exact options.auth / apikey shape is controlled.
329
- function authCtx(options, headers) {
330
- return base({ client: { options: () => options }, spec: headers == null ? null : { headers } })
334
+ function authCtx(options, spec) {
335
+ return base({ client: { options: () => options }, spec })
336
+ }
337
+
338
+ function bags() {
339
+ return { headers: {}, query: {} }
340
+ }
341
+
342
+ // `basic: false` is explicit: an HTTP Basic API's generated config carries
343
+ // `auth.basic: true`, and a client that merges it in takes a branch that
344
+ // needs a secret as well. With none supplied that branch deliberately
345
+ // writes nothing, which the probe below would read as a public API.
346
+ function auth(prefix) {
347
+ return { prefix, basic: false }
348
+ }
349
+
350
+ // Run prepareAuth with both containers present and see which one the
351
+ // generated utility writes to, and under what name. Null means this SDK
352
+ // places no credential at all — a public API — which is a legitimate
353
+ // shape, and the tests below assert exactly that instead.
354
+ //
355
+ // `pair` is the `<scheme>=` lead-in of a COOKIE credential, which rides the
356
+ // header bag under the key `cookie` instead of taking a header of its own.
357
+ function probe(options) {
358
+ const ctx = authCtx(options, bags())
359
+ stdutil.prepareAuth(ctx)
360
+ for (const where of ['headers', 'query']) {
361
+ const name = Object.keys(ctx.spec[where])[0]
362
+ if (null == name) continue
363
+ const value = ctx.spec[where][name]
364
+ const pair = 'headers' === where && 'cookie' === name
365
+ && COOKIE_PAIR.test(String(value))
366
+ ? String(value).slice(0, -1)
367
+ : ''
368
+ return { where, name, value, pair }
369
+ }
370
+ return null
371
+ }
372
+
373
+ const CRED = probe({ apikey: 'K', auth: auth('Bearer') })
374
+
375
+ // Every credential this SDK could possibly place: both credentials and
376
+ // Basic switched on, so whichever branch the API has, something lands
377
+ // unless the API is public.
378
+ const ANY = probe({ apikey: 'K', secret: 'S', auth: { prefix: 'Bearer', basic: true } })
379
+
380
+ function placed(options, seed) {
381
+ const spec = bags()
382
+ if (null != CRED && null != seed) spec[CRED.where][CRED.name] = seed
383
+ const ctx = authCtx(options, spec)
384
+ stdutil.prepareAuth(ctx)
385
+ return null == CRED ? undefined : ctx.spec[CRED.where][CRED.name]
331
386
  }
332
387
 
333
388
  test('guards a missing spec', () => {
334
- strictEqual(stdutil.prepareAuth(authCtx({ auth: { prefix: '' }, apikey: 'K' }, null)).code, 'auth_no_spec')
389
+ strictEqual(stdutil.prepareAuth(authCtx({ auth: auth(''), apikey: 'K' }, null)).code, 'auth_no_spec')
335
390
  })
336
391
 
337
- test('an apikey with a prefix is space-joined', () => {
338
- const ctx = authCtx({ apikey: 'K', auth: { prefix: 'Bearer' } }, {})
339
- stdutil.prepareAuth(ctx)
340
- strictEqual(ctx.spec.headers.authorization, 'Bearer K')
392
+ // Without this the cases below cannot fail for an SDK whose credential the
393
+ // probe misses: every one of them takes the public-API path instead.
394
+ test('the probe finds the credential this SDK places', () => {
395
+ strictEqual(null != CRED, null != ANY)
396
+ })
397
+
398
+ test('the apikey is placed where this API puts it', () => {
399
+ if (null == CRED) {
400
+ // A public API places nothing, and that is the whole assertion.
401
+ strictEqual(placed({ apikey: 'K', auth: auth('Bearer') }), undefined)
402
+ return
403
+ }
404
+ ok('headers' === CRED.where || 'query' === CRED.where)
405
+ if ('' !== CRED.pair) {
406
+ // A cookie credential is a `<scheme>=<key>` pair, and the scheme name
407
+ // leaves no room for the option's prefix.
408
+ ok(COOKIE_PAIR.test(String(CRED.value)), String(CRED.value))
409
+ return
410
+ }
411
+ // A header credential is prefix-joined; a query credential is the raw
412
+ // key, because a query parameter has nowhere to put a scheme name.
413
+ strictEqual(CRED.value, 'headers' === CRED.where ? 'Bearer K' : 'K')
341
414
  })
342
415
 
343
416
  test('a raw apikey (empty prefix) goes in as-is', () => {
344
- const ctx = authCtx({ apikey: 'K', auth: { prefix: '' } }, {})
345
- stdutil.prepareAuth(ctx)
346
- strictEqual(ctx.spec.headers.authorization, 'K')
417
+ const raw = null == CRED ? undefined : CRED.pair + 'K'
418
+ strictEqual(placed({ apikey: 'K', auth: auth('') }), raw)
347
419
  })
348
420
 
349
- test('an empty apikey drops the header', () => {
350
- const ctx = authCtx({ apikey: '', auth: { prefix: 'Bearer' } }, { authorization: 'stale' })
351
- stdutil.prepareAuth(ctx)
352
- strictEqual(ctx.spec.headers.authorization, undefined)
421
+ test('an empty apikey drops the credential', () => {
422
+ strictEqual(placed({ apikey: '', auth: auth('Bearer') }, 'stale'), undefined)
353
423
  })
354
424
 
355
- test('a public API (no auth block) drops the header', () => {
356
- const ctx = authCtx({ apikey: 'K' }, { authorization: 'stale' })
357
- stdutil.prepareAuth(ctx)
358
- strictEqual(ctx.spec.headers.authorization, undefined)
425
+ test('a public API (no auth block) drops the credential', () => {
426
+ strictEqual(placed({ apikey: 'K' }, 'stale'), undefined)
359
427
  })
360
428
 
361
- test('a missing apikey option drops the header', () => {
362
- const ctx = authCtx({ auth: { prefix: 'Bearer' } }, { authorization: 'stale' })
363
- stdutil.prepareAuth(ctx)
364
- strictEqual(ctx.spec.headers.authorization, undefined)
429
+ test('a missing apikey option drops the credential', () => {
430
+ strictEqual(placed({ auth: auth('Bearer') }, 'stale'), undefined)
365
431
  })
366
432
  })
367
433
 
@@ -79,10 +79,56 @@ describe('PrimaryUtility', async () => {
79
79
  })
80
80
 
81
81
 
82
+ function credential() {
83
+ const ctx = {
84
+ utility,
85
+ // `basic: false`: a Basic API's config defaults it true, and the
86
+ // branch it selects needs a secret, so the probe would find nothing.
87
+ client: { options: () => ({ apikey: 'PROBE', auth: { prefix: '', basic: false } }) },
88
+ spec: { headers: {}, query: {} },
89
+ error: (code, msg) => Object.assign(new Error(msg), { code }),
90
+ }
91
+ try { utility.prepareAuth(ctx) } catch (e) { return null }
92
+ for (const where of ['headers', 'query']) {
93
+ const name = Object.keys(ctx.spec[where] || {})[0]
94
+ if (null != name) return { where, name }
95
+ }
96
+ return null
97
+ }
98
+
99
+ // Rename the corpus's `headers` bag to the real container, and the
100
+ // `authorization` key inside it to the real credential name. Applied only
101
+ // to the prepareAuth section, so real header assertions elsewhere are
102
+ // untouched.
103
+ function retarget(node, cred) {
104
+ if (null == node || 'object' !== typeof node) return node
105
+ if (Array.isArray(node)) return node.map((n) => retarget(n, cred))
106
+ const out = {}
107
+ for (const key of Object.keys(node)) {
108
+ if ('headers' === key) {
109
+ const bag = {}
110
+ for (const bk of Object.keys(node[key] || {})) {
111
+ bag['authorization' === bk ? cred.name : bk] = retarget(node[key][bk], cred)
112
+ }
113
+ out[cred.where] = bag
114
+ }
115
+ else out[key] = retarget(node[key], cred)
116
+ }
117
+ return out
118
+ }
119
+
82
120
  test('auth-basic', async () => {
83
121
  const sdkopts = spec.prepareAuth?.DEF?.setup?.a || {}
84
122
  const authClient = SDK.test({}, sdkopts)
85
- await runset(spec.prepareAuth.basic, (ctx) => {
123
+
124
+ const cred = credential()
125
+ ok(null != cred, 'prepareAuth placed no credential in headers or query')
126
+
127
+ const cases = 'headers' === cred.where && 'authorization' === cred.name
128
+ ? spec.prepareAuth.basic
129
+ : retarget(spec.prepareAuth.basic, cred)
130
+
131
+ await runset(cases, (ctx) => {
86
132
  ctx.client = authClient
87
133
  fixctx(ctx)
88
134
  return utility.prepareAuth(ctx)
@@ -76,7 +76,7 @@ class Context(ctxmap: MutableMap<String, Any?>?, basectx: Context?) {
76
76
  } else if (cv is Control) {
77
77
  this.ctrl = cv
78
78
  }
79
- } else if (basectx != null) {
79
+ } else if (basectx != null && Helpers.getCtxProp(ctxmap, "opname") == null) {
80
80
  this.ctrl = basectx.ctrl
81
81
  }
82
82
 
@@ -8,8 +8,9 @@ import KOTLINPACKAGE.utility.struct.Struct
8
8
 
9
9
  // Pagination support for list operations. On the way out (PreRequest) it
10
10
  // stamps page/limit (or a cursor) into the request query; on the way back
11
- // (PreResult) it reads the server's pagination signals and records them on
12
- // `ctx.result.paging`.
11
+ // (PreResult) it reads the server's pagination signals (camelCase and
12
+ // snake_case body forms) and records them on `ctx.result.paging` and the
13
+ // caller's `ctrl.paging`.
13
14
  @Suppress("UNCHECKED_CAST")
14
15
  class PagingFeature : BaseFeature("paging", "0.0.1", true) {
15
16
 
@@ -53,7 +54,9 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
53
54
  if (cursor != null) {
54
55
  spec.query[cursorParam] = cursor
55
56
  } else if (spec.query[pageParam] == null) {
56
- val page = paging["page"]
57
+ // A record written back by preResult holds the page just fetched as
58
+ // "page" and the one to fetch as "nextPage", so nextPage wins.
59
+ val page = paging["nextPage"] ?: paging["page"]
57
60
  if (page != null) {
58
61
  spec.query[pageParam] = page
59
62
  } else {
@@ -166,18 +169,33 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
166
169
  }
167
170
  }
168
171
 
169
- // Body-level cursors.
172
+ // Body-level signals; snake_case forms are read first so camelCase wins.
170
173
  if (body is MutableMap<*, *>) {
171
174
  val bm = body as MutableMap<String, Any?>
172
175
  if (bm["next"] != null && paging["next"] == null) {
173
176
  paging["next"] = bm["next"]
174
177
  }
178
+ if (bm["next_cursor"] != null) {
179
+ paging["cursor"] = bm["next_cursor"]
180
+ }
175
181
  if (bm["cursor"] != null) {
176
182
  paging["cursor"] = bm["cursor"]
177
183
  }
178
184
  if (bm["nextCursor"] != null) {
179
185
  paging["cursor"] = bm["nextCursor"]
180
186
  }
187
+ if (paging["nextPage"] == null) {
188
+ val np = bm["nextPage"] ?: bm["next_page"]
189
+ if (np is Number) {
190
+ paging["nextPage"] = np.toInt()
191
+ } else if (np is String) {
192
+ paging["nextPage"] = np
193
+ }
194
+ }
195
+ if (bm["has_more"] is Boolean) {
196
+ paging["hasMore"] = bm["has_more"]
197
+ explicitMore = true
198
+ }
181
199
  if (bm["hasMore"] is Boolean) {
182
200
  paging["hasMore"] = bm["hasMore"]
183
201
  explicitMore = true
@@ -197,6 +215,20 @@ class PagingFeature : BaseFeature("paging", "0.0.1", true) {
197
215
 
198
216
  result.paging = paging
199
217
  this.last = paging
218
+
219
+ // The Context shares the caller's paging map by reference, so refill it
220
+ // in place; an immutable map falls back to replacing the reference.
221
+ val shared = ctx.ctrl.paging
222
+ if (shared == null) {
223
+ ctx.ctrl.paging = paging
224
+ } else {
225
+ try {
226
+ shared.clear()
227
+ shared.putAll(paging)
228
+ } catch (e: UnsupportedOperationException) {
229
+ ctx.ctrl.paging = paging
230
+ }
231
+ }
200
232
  }
201
233
 
202
234
  private fun isList(ctx: Context): Boolean {
@@ -160,20 +160,13 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
160
160
  }
161
161
  val args = buildArgs(ctx, op, updateMatch)
162
162
  val found = Struct.select(entmap, args)
163
- var ent = Struct.getelem(found, 0, null)
164
- if (ent == null) {
165
- for (e in entmap.values) {
166
- if (e is MutableMap<*, *>) {
167
- ent = e
168
- break
169
- }
170
- }
171
- }
163
+ val ent = Struct.getelem(found, 0, null)
172
164
  if (ent == null) {
165
+ // update miss: 404, never another record
173
166
  return respond(ctx, 404, null, extra("statusText", "Not found"))
174
167
  }
175
168
  if (ent is MutableMap<*, *>) {
176
- (ent as MutableMap<String, Any?>).putAll(reqdata)
169
+ Struct.merge(Struct.jt(ent, reqdata))
177
170
  }
178
171
  Struct.delprop(ent, "\$KEY")
179
172
  val out = Struct.clone(ent)
@@ -320,10 +313,14 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
320
313
  }
321
314
  }
322
315
 
323
- // Get required params.
324
- val paramsPath = Struct.getpath(point, listOf("args", "params"))
325
- val reqdParams = Struct.select(paramsPath, Struct.jm("reqd", true))
326
- val reqd = Struct.transform(reqdParams, Struct.jt("`\$EACH`", "", "`\$KEY.name`"))
316
+ // Path AND query: a path-only read misses a query-addressed record
317
+ // (e.g. GET /result?trace_id=), which has no path param at all.
318
+ val reqdArgs = Struct.jt()
319
+ for (kind in listOf("params", "query")) {
320
+ val argsPath = Struct.getpath(point, listOf("args", kind))
321
+ reqdArgs.addAll(Struct.select(argsPath, Struct.jm("reqd", true)))
322
+ }
323
+ val reqd = Struct.transform(reqdArgs, Struct.jt("`\$EACH`", "", "`\$KEY.name`"))
327
324
 
328
325
  val qand = Struct.jt()
329
326
  val q = Struct.jm("`\$AND`", qand)
@@ -6,12 +6,44 @@ import java.net.URI
6
6
  import java.net.http.HttpClient
7
7
  import java.net.http.HttpRequest
8
8
  import java.net.http.HttpResponse
9
+ import java.util.concurrent.ConcurrentHashMap
9
10
  import java.util.function.BiFunction
10
11
  import java.util.function.Supplier
11
12
 
12
13
  import KOTLINPACKAGE.core.Context
13
14
  import KOTLINPACKAGE.utility.struct.Struct
14
15
 
16
+ // Clients keyed by redirect policy and proxy. Each HttpClient owns a
17
+ // connection pool and a selector thread, so one per request reused
18
+ // nothing; one per key pools across calls.
19
+ private val clients = ConcurrentHashMap<String, HttpClient>()
20
+
21
+ private fun clientFor(fetchdef: MutableMap<String, Any?>): HttpClient {
22
+ // "manual" (set by the station feature's middleware under a hosts
23
+ // policy) returns a 3xx like any other response: an automatic follow
24
+ // would carry injected credentials to a host no policy approved.
25
+ val redirectPolicy =
26
+ if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
27
+ else HttpClient.Redirect.NORMAL
28
+
29
+ val praw = fetchdef["proxy"]
30
+ val proxy = if (praw is String) praw else ""
31
+
32
+ return clients.computeIfAbsent("$redirectPolicy|$proxy") {
33
+ val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
34
+ if ("" != proxy) {
35
+ try {
36
+ val proxyUri = URI.create(proxy)
37
+ val port = if (proxyUri.port < 0) 80 else proxyUri.port
38
+ clientb.proxy(ProxySelector.of(InetSocketAddress(proxyUri.host, port)))
39
+ } catch (e: RuntimeException) {
40
+ // Unparseable proxy target: direct connection, as the go transport.
41
+ }
42
+ }
43
+ clientb.build()
44
+ }
45
+ }
46
+
15
47
  @Suppress("UNCHECKED_CAST")
16
48
  fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): MutableMap<String, Any?> {
17
49
  var method = if (fetchdef["method"] is String) fetchdef["method"] as String else ""
@@ -50,32 +82,9 @@ fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): Mutab
50
82
  reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
51
83
  }
52
84
 
53
- // Honour a redirect annotation on the fetch definition (set by the
54
- // station feature's middleware under a hosts policy): "manual" returns
55
- // a 3xx like any other response instead of following it — the ts
56
- // donor's fetch honours the same key natively, and an automatic follow
57
- // would carry injected credentials to a host no policy approved.
58
- val redirectPolicy =
59
- if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
60
- else HttpClient.Redirect.NORMAL
61
-
62
- // Honour a proxy annotation on the fetch definition (set by the proxy
63
- // feature): route the request through a proxied HttpClient.
64
- val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
65
- val proxy = fetchdef["proxy"]
66
- if (proxy is String && "" != proxy) {
67
- try {
68
- val proxyUri = URI.create(proxy)
69
- val port = if (proxyUri.port < 0) 80 else proxyUri.port
70
- clientb.proxy(ProxySelector.of(InetSocketAddress(proxyUri.host, port)))
71
- } catch (e: RuntimeException) {
72
- // Unparseable proxy target: fall through to a direct connection.
73
- }
74
- }
75
-
76
85
  val resp: HttpResponse<String>
77
86
  try {
78
- resp = clientb.build().send(reqb.build(), HttpResponse.BodyHandlers.ofString())
87
+ resp = clientFor(fetchdef).send(reqb.build(), HttpResponse.BodyHandlers.ofString())
79
88
  } catch (e: Exception) {
80
89
  throw RuntimeException("fetch: " + e.message, e)
81
90
  }
@@ -194,7 +194,7 @@ fun prepareQuery(ctx: Context): MutableMap<String, Any?> {
194
194
  for (item in Struct.items(reqmatch)) {
195
195
  val key = if (item[0] is String) item[0] as String else ""
196
196
  val v = item[1]
197
- if (v != null && !containsStr(params, key)) {
197
+ if (v != null && "\$action" != key && !containsStr(params, key)) {
198
198
  out[key] = v
199
199
  }
200
200
  }
@@ -58,21 +58,36 @@ fun resultHeaders(ctx: Context): KOTLINPACKAGE.core.Result {
58
58
  return result!!
59
59
  }
60
60
 
61
+ // `$action` selects the point (see makePoint); it is never an API field, so
62
+ // the body is a copy without it. The caller's map is left untouched.
63
+ private fun stripAction(reqdata: Any?): Any? {
64
+ if (reqdata !is Map<*, *> || !reqdata.containsKey("\$action")) {
65
+ return reqdata
66
+ }
67
+ val body = linkedMapOf<String, Any?>()
68
+ for ((k, v) in reqdata) {
69
+ if ("\$action" != k) {
70
+ body[k.toString()] = v
71
+ }
72
+ }
73
+ return body
74
+ }
75
+
61
76
  fun transformRequest(ctx: Context): Any? {
62
77
  if (ctx.spec != null) {
63
78
  ctx.spec!!.step = "reqform"
64
79
  }
65
80
 
66
81
  val transform = Helpers.toMapAny(Struct.getprop(ctx.point, "transform"))
67
- ?: return ctx.reqdata
82
+ ?: return stripAction(ctx.reqdata)
68
83
 
69
84
  val reqform = Struct.getprop(transform, "req", null)
70
- ?: return ctx.reqdata
85
+ ?: return stripAction(ctx.reqdata)
71
86
 
72
87
  val data = linkedMapOf<String, Any?>()
73
88
  data["reqdata"] = ctx.reqdata
74
89
 
75
- return Struct.transform(data, reqform)
90
+ return stripAction(Struct.transform(data, reqform))
76
91
  }
77
92
 
78
93
  fun transformResponse(ctx: Context): Any? {
@@ -49,7 +49,14 @@ function Context.new(ctxmap, basectx)
49
49
  if type(ctrl_raw.explain) == "table" then
50
50
  self.ctrl.explain = ctrl_raw.explain
51
51
  end
52
- elseif basectx ~= nil and basectx.ctrl ~= nil then
52
+ if ctrl_raw.actor ~= nil then
53
+ self.ctrl.actor = ctrl_raw.actor
54
+ end
55
+ if type(ctrl_raw.paging) == "table" then
56
+ self.ctrl.paging = ctrl_raw.paging
57
+ end
58
+ elseif basectx ~= nil and basectx.ctrl ~= nil
59
+ and helpers.get_ctx_prop(ctxmap, "opname") == nil then
53
60
  self.ctrl = basectx.ctrl
54
61
  end
55
62
 
@@ -10,6 +10,9 @@ function Control.new(opts)
10
10
  self.throw_err = opts.throw_err
11
11
  self.err = nil
12
12
  self.explain = opts.explain
13
+ -- Per-call feature inputs (audit actor, paging cursor/page).
14
+ self.actor = opts.actor
15
+ self.paging = opts.paging
13
16
  return self
14
17
  end
15
18