@voxgig/sdkgen 4.4.1 → 4.5.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 (104) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/FeatureDocs.d.ts +16 -0
  3. package/dist/cmp/FeatureDocs.js +72 -0
  4. package/dist/cmp/FeatureDocs.js.map +1 -0
  5. package/dist/cmp/Readme.js +2 -0
  6. package/dist/cmp/Readme.js.map +1 -1
  7. package/dist/cmp/ReadmeFeatures.d.ts +2 -0
  8. package/dist/cmp/ReadmeFeatures.js +95 -0
  9. package/dist/cmp/ReadmeFeatures.js.map +1 -0
  10. package/dist/cmp/ReadmeRef.js +4 -0
  11. package/dist/cmp/ReadmeRef.js.map +1 -1
  12. package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
  13. package/dist/cmp/ReadmeRefFeatures.js +130 -0
  14. package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
  15. package/dist/cmp/ReadmeTop.js +46 -0
  16. package/dist/cmp/ReadmeTop.js.map +1 -1
  17. package/dist/helpers/opShape.js +5 -1
  18. package/dist/helpers/opShape.js.map +1 -1
  19. package/dist/sdkgen.d.ts +3 -2
  20. package/dist/sdkgen.js +30 -10
  21. package/dist/sdkgen.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/utility.d.ts +2 -1
  24. package/dist/utility.js +17 -1
  25. package/dist/utility.js.map +1 -1
  26. package/package.json +1 -1
  27. package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
  28. package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
  29. package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
  30. package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
  31. package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
  32. package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
  33. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
  34. package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
  35. package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
  36. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
  37. package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
  38. package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
  39. package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
  40. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
  41. package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
  42. package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
  43. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
  44. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
  45. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
  46. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
  47. package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
  48. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
  49. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
  50. package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
  51. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  52. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
  53. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
  54. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
  55. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
  56. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
  57. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
  58. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
  59. package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
  60. package/project/.sdk/tm/clojure/src/sdk/core.clj +6 -2
  61. package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
  62. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +5 -0
  63. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +4 -0
  64. package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
  65. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +4 -1
  66. package/project/.sdk/tm/go/feature/test_feature.go +17 -6
  67. package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
  68. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
  69. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
  70. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
  71. package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
  72. package/project/.sdk/tm/js/test/feature.test.js +19 -9
  73. package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
  74. package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
  75. package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
  76. package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
  77. package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
  78. package/project/.sdk/tm/ocaml/sdk_runtime.ml +4 -1
  79. package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
  80. package/project/.sdk/tm/perl/utility/make_options.pm +4 -1
  81. package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
  82. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
  83. package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
  84. package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
  85. package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
  86. package/project/.sdk/tm/py/test/test_feature.py +40 -0
  87. package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
  88. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
  89. package/project/.sdk/tm/rb/utility/make_options.rb +4 -1
  90. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
  91. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
  92. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
  93. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
  94. package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
  95. package/project/sdkgen-package.json +1 -1
  96. package/src/cmp/FeatureDocs.ts +100 -0
  97. package/src/cmp/Readme.ts +2 -0
  98. package/src/cmp/ReadmeFeatures.ts +113 -0
  99. package/src/cmp/ReadmeRef.ts +4 -0
  100. package/src/cmp/ReadmeRefFeatures.ts +149 -0
  101. package/src/cmp/ReadmeTop.ts +48 -0
  102. package/src/helpers/opShape.ts +5 -1
  103. package/src/sdkgen.ts +28 -7
  104. package/src/utility.ts +21 -1
@@ -17,6 +17,18 @@ import {
17
17
  // generated provider is verified without a server.
18
18
 
19
19
 
20
+ // Does this entity's load op have a real identifying param (path or
21
+ // required query), e.g. GET /result?trace_id=? A paramless GET has none.
22
+ function loadHasKey(ent: any): boolean {
23
+ const point = (ent.op && ent.op.load && ent.op.load.points || [])[0]
24
+ if (null == point) return false
25
+ const hasPathParam = (point.parts || []).some((p: string) => p.startsWith('{'))
26
+ const hasQueryParam = (point.args && point.args.query || [])
27
+ .some((q: any) => false !== q.reqd)
28
+ return hasPathParam || hasQueryParam
29
+ }
30
+
31
+
20
32
  // The name of the entity a parent path param addresses, or '' when the model
21
33
  // has none of that name.
22
34
  //
@@ -458,7 +470,12 @@ describe('${provider.fileBase}', () => {
458
470
  )
459
471
  })
460
472
 
461
-
473
+ `)
474
+ // Paramless read (e.g. GET /usage): every id "misses" the same
475
+ // way a hit does -- the mock has nothing to filter by -- so a
476
+ // load-missing test would just assert the happy path again.
477
+ if (loadHasKey(e.ent)) {
478
+ Content(`
462
479
  // A 404 from a single-item read is an ordinary "not found" answer, not a
463
480
  // failure: the provider turns it into null rather than letting the SDK
464
481
  // throw.
@@ -472,6 +489,7 @@ describe('${provider.fileBase}', () => {
472
489
  })
473
490
 
474
491
  `)
492
+ }
475
493
  }
476
494
  })
477
495
 
@@ -4,7 +4,7 @@ import {
4
4
  entityCollection, entityOps, entityIdField, entityClassName,
5
5
  opRequestShape, opParams, ownPoint, entityPath,
6
6
  collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
7
- authorInfo, contributorList, isAuthActive, jsKey, jsProp,
7
+ authorInfo, contributorList, isAuthActive, isHttpBasicAuth, jsKey, jsProp,
8
8
  SdkGenError,
9
9
  PUBLISHER, PUBLISHER_URL,
10
10
  } from '@voxgig/sdkgen'
@@ -108,6 +108,14 @@ function recordKey(ent: any): string {
108
108
  if (null != lastParam) {
109
109
  return lastParam.slice(1, -1)
110
110
  }
111
+
112
+ // No path param at all (e.g. GET /scan/async/result?trace_id=...): the
113
+ // record's own key can still be a single required QUERY param.
114
+ const query = (point && point.args && point.args.query) || []
115
+ const reqdQuery = query.filter((q: any) => false !== q.reqd)
116
+ if (1 === reqdQuery.length) {
117
+ return String(reqdQuery[0].name)
118
+ }
111
119
  }
112
120
 
113
121
  return 'id'
@@ -462,6 +470,13 @@ const Main = cmp(function Main(props: any) {
462
470
  // model and strips the authorization header regardless of options, so
463
471
  // plumbing one anyway produces a credential path that cannot work.
464
472
  authActive: isAuthActive(model),
473
+ // Whether this API's scheme is genuine HTTP Basic Auth (two credentials,
474
+ // base64-joined), matching the SDK's own auth.basic signal. Decides
475
+ // whether the plugin also plumbs a `secret` alongside `apikey` — a
476
+ // Basic-Auth SDK's own auth stage deletes the header when either is
477
+ // missing, so a provider forwarding apikey alone could never
478
+ // authenticate, the same class of gap `apikey`-only forwarding was.
479
+ authBasic: isAuthActive(model) && isHttpBasicAuth(model),
465
480
  }
466
481
 
467
482
  // `.gitignore` is EMITTED rather than copied — npm strips that filename
@@ -895,7 +910,17 @@ ${provider.authActive ? `
895
910
  if (null != apikey && '' !== apikey) {
896
911
  sdkopts.apikey = apikey
897
912
  }
898
- ` : `
913
+ ${provider.authBasic ? `
914
+ // Genuine HTTP Basic Auth needs a SECOND credential (the SDK sends
915
+ // \`Authorization: Basic base64(apikey:secret)\`) — without it the SDK's
916
+ // auth stage treats the pair as incomplete and deletes the header, same
917
+ // as a missing apikey.
918
+ const secret = res?.keymap?.secret?.value
919
+
920
+ if (null != secret && '' !== secret) {
921
+ sdkopts.secret = secret
922
+ }
923
+ ` : ''}` : `
899
924
  // This API declares no authentication, so no credential is plumbed. The
900
925
  // SDK's auth stage emits nothing for an auth-inactive model and deletes
901
926
  // any \`authorization\` header regardless of options, so a keymap lookup
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { cmp, each, Content, canonToType, canonKey, canonScalarKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -376,6 +377,10 @@ let client = ${SDK}(options)
376
377
  \`\`\`
377
378
 
378
379
  `)
380
+ // The shared feature reference: options, defaults, usage and the
381
+ // considerations. Model facts, identical in every target, so they are
382
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
383
+ ReadmeRefFeatures({ target })
379
384
  }
380
385
 
381
386
  })
@@ -15,6 +15,7 @@ import {
15
15
  indent,
16
16
  isAuthActive,
17
17
  isConfigData,
18
+ isHttpBasicAuth,
18
19
  resolveAuthPrefix,
19
20
  serverVariables,
20
21
  } from '@voxgig/sdkgen'
@@ -49,9 +50,11 @@ const Config = cmp(async function Config(props: any) {
49
50
  const authActive = isAuthActive(model)
50
51
  // config.auth.prefix override -> spec-derived info.security.prefix -> 'Bearer'
51
52
  const authPrefix = resolveAuthPrefix(model)
53
+ const authBasic = isHttpBasicAuth(model)
52
54
  const authBlock = authActive
53
55
  ? `auth: {
54
- prefix: '${authPrefix}',
56
+ prefix: '${authPrefix}',${authBasic ? `
57
+ basic: true,` : ''}
55
58
  },
56
59
 
57
60
  `
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, envName, entityIdField, entityDataIdField, pickExampleEntity, opRequestShape, safeVarName, exampleVarName, jsKey } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, isHttpBasicAuth, envName, entityIdField, entityDataIdField, pickExampleEntity, opRequestShape, safeVarName, exampleVarName, jsKey } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -92,11 +92,16 @@ ${stateDataLine}
92
92
  : ''
93
93
 
94
94
  const authActive = isAuthActive(model)
95
+ const authBasic = authActive && isHttpBasicAuth(model)
95
96
  const apikeyTesterCtor = authActive
96
- ? `new ${model.const.Name}SDK({ apikey: '...' })`
97
+ ? `new ${model.const.Name}SDK({ apikey: '...'${authBasic ? `, secret: '...'` : ''} })`
97
98
  : `new ${model.const.Name}SDK()`
98
- const apikeyExtendField = authActive ? `\n apikey: '...',` : ''
99
- const apikeyEnvLine = authActive ? `\n${envName(model)}_APIKEY=<your-key>` : ''
99
+ const apikeyExtendField = authActive
100
+ ? `\n apikey: '...',${authBasic ? `\n secret: '...',` : ''}`
101
+ : ''
102
+ const apikeyEnvLine = authActive
103
+ ? `\n${envName(model)}_APIKEY=<your-key>${authBasic ? `\n${envName(model)}_SECRET=<your-secret>` : ''}`
104
+ : ''
100
105
 
101
106
  Content(`### Make a direct HTTP request
102
107
 
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, isHttpBasicAuth } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -51,17 +51,22 @@ const ReadmeModel = cmp(function ReadmeModel(props: any) {
51
51
  const returnBullets = retBullets.join('\n')
52
52
 
53
53
  const authActive = isAuthActive(model)
54
+ const authBasic = authActive && isHttpBasicAuth(model)
54
55
  const apikeyOptionType = authActive ? `\n apikey?: string` : ''
56
+ const secretOptionType = authBasic ? `\n secret?: string` : ''
55
57
  const apikeyOptionRow = authActive
56
58
  ? '| `apikey` | `string` | API key for authentication. |\n'
57
59
  : ''
60
+ const secretOptionRow = authBasic
61
+ ? '| `secret` | `string` | API secret for authentication. |\n'
62
+ : ''
58
63
 
59
64
  Content(`### ${model.const.Name}SDK
60
65
 
61
66
  #### Constructor
62
67
 
63
68
  \`\`\`ts
64
- new ${model.const.Name}SDK(options?: {${apikeyOptionType}
69
+ new ${model.const.Name}SDK(options?: {${apikeyOptionType}${secretOptionType}
65
70
  base?: string
66
71
  prefix?: string
67
72
  suffix?: string
@@ -72,7 +77,7 @@ new ${model.const.Name}SDK(options?: {${apikeyOptionType}
72
77
 
73
78
  | Option | Type | Description |
74
79
  | --- | --- | --- |
75
- ${apikeyOptionRow}| \`base\` | \`string\` | Base URL of the API server. |
80
+ ${apikeyOptionRow}${secretOptionRow}| \`base\` | \`string\` | Base URL of the API server. |
76
81
  | \`prefix\` | \`string\` | URL path prefix prepended to all requests. |
77
82
  | \`suffix\` | \`string\` | URL path suffix appended to all requests. |
78
83
  | \`feature\` | \`object\` | Feature activation flags (e.g. \`{ test: { active: true } }\`). |
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive, packageName, envName, opRequestShape, entityIdField, entityDataIdField, entityOps, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, isHttpBasicAuth, packageName, envName, opRequestShape, entityIdField, entityDataIdField, entityOps, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -41,7 +41,8 @@ const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
41
41
  ) as any
42
42
 
43
43
  const ctor = isAuthActive(model)
44
- ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,\n})`
44
+ ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,${
45
+ isHttpBasicAuth(model) ? `\n secret: process.env.${envName(model)}_SECRET,` : ''}\n})`
45
46
  : `new ${model.const.Name}SDK()`
46
47
 
47
48
  Content(`### 1. Create a client
@@ -1,5 +1,6 @@
1
1
 
2
- import { cmp, each, Content, canonToType, File, isAuthActive, entityIdField, entityActions, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, File, isAuthActive, isHttpBasicAuth, entityIdField, entityActions, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -83,7 +84,7 @@ Create a new SDK client instance.
83
84
  | Name | Type | Description |
84
85
  | --- | --- | --- |
85
86
  | \`options\` | \`object\` | SDK configuration options. |
86
- ${isAuthActive(model) ? '| \`options.apikey\` | \`string\` | API key for authentication. |\n' : ''}| \`options.base\` | \`string\` | Base URL for API requests. |
87
+ ${isAuthActive(model) ? '| \`options.apikey\` | \`string\` | API key for authentication. |\n' : ''}${isAuthActive(model) && isHttpBasicAuth(model) ? '| \`options.secret\` | \`string\` | API secret for authentication. |\n' : ''}| \`options.base\` | \`string\` | Base URL for API requests. |
87
88
  | \`options.prefix\` | \`string\` | URL prefix appended after base. |
88
89
  | \`options.suffix\` | \`string\` | URL suffix appended after path. |
89
90
  | \`options.headers\` | \`object\` | Custom headers for all requests. |
@@ -454,6 +455,10 @@ const client = new ${model.Name}SDK({
454
455
  \`\`\`
455
456
 
456
457
  `)
458
+ // The shared feature reference: options, defaults, usage and the
459
+ // considerations. Model facts, identical in every target, so they are
460
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
461
+ ReadmeRefFeatures({ target })
457
462
  }
458
463
 
459
464
  })
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, packageName, envName, entityIdField, entityOps, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, isHttpBasicAuth, packageName, envName, entityIdField, entityOps, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -29,7 +29,8 @@ const ReadmeTopQuick = cmp(function ReadmeTopQuick(props: any) {
29
29
 
30
30
  const authActive = isAuthActive(model)
31
31
  const ctor = authActive
32
- ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,\n})`
32
+ ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,${
33
+ isHttpBasicAuth(model) ? `\n secret: process.env.${envName(model)}_SECRET,` : ''}\n})`
33
34
  : `new ${model.const.Name}SDK()`
34
35
 
35
36
  Content(`\`\`\`ts
@@ -17,6 +17,7 @@ import {
17
17
  cmp,
18
18
  snakify,
19
19
  isAuthActive,
20
+ isHttpBasicAuth,
20
21
  jsProp,
21
22
  jsOptProp, envName, envToken, liveStrict
22
23
  } from '@voxgig/sdkgen'
@@ -44,12 +45,14 @@ const TestDirect = cmp(function TestDirect(props: any) {
44
45
  const entidEnvVar = `${PROJECTNAME}_TEST_${envToken(entity.name)}_ENTID`
45
46
 
46
47
  const authActive = isAuthActive(model)
48
+ const authBasic = authActive && isHttpBasicAuth(model)
47
49
  const apikeyEnvEntry = authActive
48
- ? `\n '${PROJECTNAME}_APIKEY': 'NONE',`
50
+ ? `\n '${PROJECTNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJECTNAME}_SECRET': 'NONE',` : ''}`
49
51
  : ''
50
52
  const apikeyLiveField = authActive
51
53
  ? `
52
- apikey: env.${PROJECTNAME}_APIKEY,`
54
+ apikey: env.${PROJECTNAME}_APIKEY,${authBasic ? `
55
+ secret: env.${PROJECTNAME}_SECRET,` : ''}`
53
56
  : ''
54
57
 
55
58
  const opnames = Object.keys(entity.op || {})
@@ -30,6 +30,7 @@ import {
30
30
  cmp,
31
31
  each,
32
32
  isAuthActive,
33
+ isHttpBasicAuth,
33
34
  entityDataIdField, envName, envToken
34
35
  } from '@voxgig/sdkgen'
35
36
 
@@ -64,12 +65,14 @@ const TestEntity = cmp(function TestEntity(props: any) {
64
65
  const PROJENVNAME = envName(model)
65
66
  const ENTENVNAME = envToken(entity.name)
66
67
  const authActive = isAuthActive(model)
68
+ const authBasic = authActive && isHttpBasicAuth(model)
67
69
  const apikeyEnvEntry = authActive
68
- ? `\n '${PROJENVNAME}_APIKEY': 'NONE',`
70
+ ? `\n '${PROJENVNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
69
71
  : ''
70
72
  const apikeyLiveField = authActive
71
73
  ? `
72
- apikey: env.${PROJENVNAME}_APIKEY,`
74
+ apikey: env.${PROJENVNAME}_APIKEY,${authBasic ? `
75
+ secret: env.${PROJENVNAME}_SECRET,` : ''}`
73
76
  : ''
74
77
 
75
78
  // TODO: should be a utility function
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { cmp, each, Content, canonKey, canonScalarKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -389,6 +390,10 @@ const client = sdk.${model.const.Name}SDK.new(h.jo(&.{
389
390
  \`\`\`
390
391
 
391
392
  `)
393
+ // The shared feature reference: options, defaults, usage and the
394
+ // considerations. Model facts, identical in every target, so they are
395
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
396
+ ReadmeRefFeatures({ target })
392
397
  }
393
398
 
394
399
  })
@@ -1012,8 +1012,12 @@
1012
1012
  config (or (oget ctx :config) (vs/jm))
1013
1013
  cfgopts (let [c (vs/getprop config "options")] (if (vs/ismap c) c (vs/jm)))
1014
1014
  optspec (vs/jm
1015
- "apikey" "" "base" "http://localhost:8000" "prefix" "" "suffix" ""
1016
- "auth" (vs/jm "prefix" "")
1015
+ "apikey" "" "secret" "" "base" "http://localhost:8000" "prefix" "" "suffix" ""
1016
+ ;; `basic` and `secret`: HTTP Basic Auth needs a second
1017
+ ;; credential and a flag to say the pair is Basic rather
1018
+ ;; than a single bearer token. Absent from the shape, a
1019
+ ;; client passing them is refused with "Unexpected keys".
1020
+ "auth" (vs/jm "prefix" "" "basic" false)
1017
1021
  "headers" (vs/jm "`$CHILD`" "`$STRING`")
1018
1022
  "allow" (vs/jm "method" "GET,PUT,POST,PATCH,DELETE,OPTIONS"
1019
1023
  "op" "create,update,load,list,remove,command,direct,graphql")
@@ -619,6 +619,180 @@
619
619
  )
620
620
  fa))
621
621
 
622
+ ;; ---------------------------------------------------------------------------
623
+ ;; cost
624
+ ;; ---------------------------------------------------------------------------
625
+ ;; Prices every transport ATTEMPT and commits the spend once per OPERATION.
626
+ ;; Mirrors tm/ts/src/feature/cost/CostFeature.ts; the cases are
627
+ ;; .sdk/test/feature/cost.aon.
628
+ ;;
629
+ ;; ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
630
+ ;; cache is charged for money that was never spent. Activate in list form with
631
+ ;; cost first: [{name "cost"} {name "cache"}].
632
+
633
+ (defn cost-feature []
634
+ (let [fa (new-feature "cost" false "0.0.1")
635
+ per-unit (fn [] (let [p (opt fa "perUnit")] (if (number? p) p 0)))
636
+ budget-limit (fn [] (let [b (opt fa "budget")] (if (number? b) b 0)))
637
+ record! (fn []
638
+ (track-ensure! fa "_cost"
639
+ (vs/jm "currency" (or (opt fa "currency") "USD")
640
+ "total" (vs/jm "calls" 0 "attempts" 0 "amount" 0 "reported" 0 "estimated" 0)
641
+ "ops" (vs/jm) "actors" (vs/jm)
642
+ "budget" (vs/jm "limit" (budget-limit) "spent" 0
643
+ "remaining" (budget-limit) "exceeded" false)
644
+ "last" nil)))
645
+ bump! (fn [m k by] (.put ^java.util.Map m k (+ (or (mget m k) 0) by)))
646
+ ;; Accumulated per context, committed once at PreDone: adding each
647
+ ;; attempt to the running total and subtracting it again when a body
648
+ ;; figure supersedes it loses precision to catastrophic cancellation.
649
+ pending (fn [ctx]
650
+ (let [id (core/oget ctx :id)]
651
+ (or (get (:pending @fa) id)
652
+ (let [p (vs/jm "attempts" 0 "amount" 0 "reported" 0 "estimated" 0
653
+ "source" "none" "piped" false)]
654
+ (swap! fa update :pending assoc id p) p))))
655
+ price-header (fn [res]
656
+ (let [name (opt fa "header")]
657
+ (when (and (string? name) (seq name))
658
+ (let [v (header-of (vs/getprop res "headers") name)
659
+ n (to-num v)]
660
+ (when (number? n) [(* n (per-unit)) "header"])))))
661
+ ;; Same lookup grammar as rbac's rules: '<entity>.<op>', then '<op>',
662
+ ;; then '*'.
663
+ price-rate (fn [ctx]
664
+ (let [rates (let [r (opt fa "rates")] (if (vs/ismap r) r (vs/jm)))
665
+ op (core/oget ctx :op)
666
+ ent (or (when-let [e (core/oget ctx :entity)] (core/entity-get-name e))
667
+ (when op (core/op-entity op)) "")
668
+ opname (if op (core/op-name op) "")
669
+ pick (fn [k] (let [v (vs/getprop rates k)] (when (number? v) v)))]
670
+ (when-let [n (or (pick (str ent "." opname)) (pick opname) (pick "*"))]
671
+ [n "table"])))
672
+ price (fn [ctx res]
673
+ (or (price-header res)
674
+ (price-rate ctx)
675
+ (let [u (opt fa "unit")]
676
+ (if (and (number? u) (not= 0 u)) [u "unit"] [0 "none"]))))
677
+ ;; A usage figure from the parsed result body, priced by perUnit. Read
678
+ ;; at commit, not at the transport seam, because the body is one-shot.
679
+ price-body (fn [ctx]
680
+ (let [path (opt fa "path")]
681
+ (when (and (string? path) (seq path))
682
+ (when-let [result (core/oget ctx :result)]
683
+ (let [body (core/oget result :body)]
684
+ (when (vs/ismap body)
685
+ (let [n (to-num (vs/getpath body path))]
686
+ (when (number? n) (* n (per-unit))))))))))
687
+ spend! (fn [rec amount reported estimated]
688
+ (let [total (mget rec "total") budget (mget rec "budget")
689
+ lim (or (mget budget "limit") 0)]
690
+ (bump! total "amount" amount)
691
+ (bump! total "reported" reported)
692
+ (bump! total "estimated" estimated)
693
+ (let [spent (or (mget total "amount") 0)]
694
+ (.put ^java.util.Map budget "spent" spent)
695
+ (.put ^java.util.Map budget "remaining"
696
+ (if (> lim 0) (max 0 (- lim spent)) 0))
697
+ (when (and (> lim 0) (>= spent lim))
698
+ (.put ^java.util.Map budget "exceeded" true)))))
699
+ bump-bucket! (fn [bucket key amount]
700
+ (let [b (let [x (vs/getprop bucket key)]
701
+ (if (vs/ismap x) x
702
+ (let [m (vs/jm "calls" 0 "amount" 0)]
703
+ (.put ^java.util.Map bucket key m) m)))]
704
+ (bump! b "calls" 1)
705
+ (bump! b "amount" amount)))
706
+ commit! (fn [ctx p entity opname]
707
+ (let [rec (record!)
708
+ body (price-body ctx)
709
+ amount (if (some? body) body (or (mget p "amount") 0))
710
+ reported (if (some? body) body (or (mget p "reported") 0))
711
+ estimated (if (some? body) 0 (or (mget p "estimated") 0))
712
+ source (if (some? body) "body" (mget p "source"))
713
+ actor (or (core/oget (core/oget ctx :ctrl) :actor)
714
+ (opt fa "actor") "anonymous")]
715
+ (spend! rec amount reported estimated)
716
+ (bump! (mget rec "total") "calls" 1)
717
+ (bump-bucket! (mget rec "ops") (str entity "." opname) amount)
718
+ (bump-bucket! (mget rec "actors") (str actor) amount)
719
+ (swap! fa update :seq (fnil inc 0))
720
+ (.put ^java.util.Map rec "last"
721
+ (vs/jm "seq" (:seq @fa) "entity" entity "op" opname "actor" (str actor)
722
+ "amount" amount "currency" (mget rec "currency")
723
+ "source" source "attempts" (mget p "attempts")))))
724
+ finish! (fn [ctx done]
725
+ (when (active? fa)
726
+ (let [id (core/oget ctx :id) p (get (:pending @fa) id)]
727
+ (when (some? p)
728
+ (swap! fa update :pending dissoc id)
729
+ ;; A FAILED operation that made no attempt never reached
730
+ ;; the network - the budget gate refused it - and must
731
+ ;; not be counted. A SUCCEEDED one that made no attempt
732
+ ;; was served from the cache: a real call that cost
733
+ ;; nothing, which is the point of ordering cost inside
734
+ ;; the cache.
735
+ (when (or done (> (or (mget p "attempts") 0) 0))
736
+ (let [op (core/oget ctx :op)
737
+ entity (if (and op (seq (core/op-entity op))) (core/op-entity op) "_")
738
+ opname (if (and op (seq (core/op-name op))) (core/op-name op) "_")]
739
+ (commit! ctx p entity opname)))))))
740
+ charge (fn [ctx url fetchdef inner]
741
+ ;; A rejecting transport still costs an attempt: without this a
742
+ ;; run of connection-level failures under retry would be
743
+ ;; charged nothing, and an onBudget "deny" ceiling could never
744
+ ;; stop it.
745
+ (let [[res err] (inner ctx url fetchdef)
746
+ [amount source] (price ctx res)
747
+ p (pending ctx)]
748
+ (bump! p "attempts" 1)
749
+ (bump! p "amount" amount)
750
+ (bump! p (if (or (= source "header") (= source "body")) "reported" "estimated") amount)
751
+ (.put ^java.util.Map p "source" source)
752
+ (bump! (mget (record!) "total") "attempts" 1)
753
+ ;; direct() and graphql() dispatch no pipeline hooks, so
754
+ ;; their spend is committed here or never. `piped` is set by
755
+ ;; PrePoint, so its absence is the signal.
756
+ (when (not= true (mget p "piped"))
757
+ (swap! fa update :pending dissoc (core/oget ctx :id))
758
+ (commit! ctx p "_" "direct"))
759
+ [res err]))]
760
+ (swap! fa assoc :pending {} :seq 0)
761
+ (swap! fa assoc
762
+ "init" (fn [ctx options]
763
+ (swap! fa assoc :client (core/oget ctx :client)
764
+ :options (if (vs/ismap options) options (vs/jm))
765
+ :active (opts-active? options) :pending {} :seq 0)
766
+ (when (active? fa)
767
+ (record!)
768
+ (wrap-fetcher! ctx (fn [fctx url fd inner] (charge fctx url fd inner)))))
769
+ "PrePoint"
770
+ (fn [ctx]
771
+ (when (active? fa)
772
+ ;; Mark the context as piped, so charge knows a PreDone follows.
773
+ (.put ^java.util.Map (pending ctx) "piped" true)
774
+ (let [lim (budget-limit)]
775
+ (when (> lim 0)
776
+ (let [rec (record!)
777
+ spent (or (mget (mget rec "total") "amount") 0)]
778
+ (when (>= spent lim)
779
+ (.put ^java.util.Map (mget rec "budget") "exceeded" true)
780
+ (when (= "deny" (opt fa "onBudget"))
781
+ (let [err (core/ctx-error ctx "cost_budget"
782
+ (str "Cost budget of " lim " " (mget rec "currency")
783
+ " is spent (" spent " used)"))]
784
+ (core/out-set! ctx "point" err)
785
+ err))))))))
786
+ "PreDone" (fn [ctx] (finish! ctx true))
787
+ ;; A failed operation never reaches PreDone - make-error dispatches
788
+ ;; PreUnexpected instead - so without this its attempts are priced
789
+ ;; and then discarded: repeated connection failures would slip past
790
+ ;; an onBudget "deny" ceiling and leak the pending entry. `false`
791
+ ;; keeps a call that made NO attempt (refused at the budget gate)
792
+ ;; from being counted.
793
+ "PreUnexpected" (fn [ctx] (finish! ctx false)))
794
+ fa))
795
+
622
796
  ;; ---------------------------------------------------------------------------
623
797
  ;; streaming
624
798
  ;; ---------------------------------------------------------------------------
@@ -1108,4 +1282,5 @@
1108
1282
  "clienttrack" (clienttrack-feature)
1109
1283
  "rbac" (rbac-feature)
1110
1284
  "netsim" (netsim-feature)
1285
+ "cost" (cost-feature)
1111
1286
  (base-feature)))
@@ -150,12 +150,17 @@ public static partial class SdkUtility
150
150
  var optspec = new Dictionary<string, object?>
151
151
  {
152
152
  ["apikey"] = "",
153
+ ["secret"] = "",
153
154
  ["base"] = "http://localhost:8000",
154
155
  ["prefix"] = "",
155
156
  ["suffix"] = "",
157
+ // `basic` and `secret`: HTTP Basic Auth needs a second credential
158
+ // and a flag to say the pair is Basic rather than a single bearer
159
+ // token.
156
160
  ["auth"] = new Dictionary<string, object?>
157
161
  {
158
162
  ["prefix"] = "",
163
+ ["basic"] = false,
159
164
  },
160
165
  ["headers"] = new Dictionary<string, object?>
161
166
  {
@@ -21,10 +21,14 @@ dynamic makeOptions(dynamic ctx) {
21
21
  final optspec = {
22
22
  'apikey': '',
23
23
  'base': 'http://localhost:8000',
24
+ 'secret': '',
24
25
  'prefix': '',
25
26
  'suffix': '',
27
+ // `basic` and `secret`: HTTP Basic Auth needs a second credential and a
28
+ // flag to say the pair is Basic rather than a single bearer token.
26
29
  'auth': {
27
30
  'prefix': '',
31
+ 'basic': false,
28
32
  },
29
33
  'headers': {
30
34
  '`\$CHILD`': '`\$STRING`',