@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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/FeatureDocs.d.ts +16 -0
- package/dist/cmp/FeatureDocs.js +72 -0
- package/dist/cmp/FeatureDocs.js.map +1 -0
- package/dist/cmp/Readme.js +2 -0
- package/dist/cmp/Readme.js.map +1 -1
- package/dist/cmp/ReadmeFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeFeatures.js +95 -0
- package/dist/cmp/ReadmeFeatures.js.map +1 -0
- package/dist/cmp/ReadmeRef.js +4 -0
- package/dist/cmp/ReadmeRef.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
- package/dist/cmp/ReadmeRefFeatures.js +130 -0
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
- package/dist/cmp/ReadmeTop.js +46 -0
- package/dist/cmp/ReadmeTop.js.map +1 -1
- package/dist/helpers/opShape.js +5 -1
- package/dist/helpers/opShape.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +30 -10
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +17 -1
- package/dist/utility.js.map +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
- package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
- package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
- package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
- package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
- package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
- package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
- package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
- package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
- package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
- package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
- package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
- package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
- package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
- package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
- package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
- package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
- package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
- package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
- package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
- package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
- package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
- package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
- package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
- package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
- package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
- package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
- package/project/.sdk/tm/clojure/src/sdk/core.clj +6 -2
- package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +5 -0
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +4 -0
- package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +4 -1
- package/project/.sdk/tm/go/feature/test_feature.go +17 -6
- package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
- package/project/.sdk/tm/js/test/feature.test.js +19 -9
- package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
- package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
- package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
- package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
- package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +4 -1
- package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
- package/project/.sdk/tm/perl/utility/make_options.pm +4 -1
- package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
- package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
- package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
- package/project/.sdk/tm/py/test/test_feature.py +40 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
- package/project/.sdk/tm/rb/utility/make_options.rb +4 -1
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
- package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
- package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/FeatureDocs.ts +100 -0
- package/src/cmp/Readme.ts +2 -0
- package/src/cmp/ReadmeFeatures.ts +113 -0
- package/src/cmp/ReadmeRef.ts +4 -0
- package/src/cmp/ReadmeRefFeatures.ts +149 -0
- package/src/cmp/ReadmeTop.ts +48 -0
- package/src/helpers/opShape.ts +5 -1
- package/src/sdkgen.ts +28 -7
- 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
|
|
99
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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`',
|