@voxgig/sdkgen 4.17.6 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/AgentGuideContent.js +30 -0
- package/dist/cmp/AgentGuideContent.js.map +1 -1
- package/dist/cmp/FeatureDocs.d.ts +6 -1
- package/dist/cmp/FeatureDocs.js +27 -0
- package/dist/cmp/FeatureDocs.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.js +23 -13
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
- package/dist/helpers/applicability.js +7 -0
- package/dist/helpers/applicability.js.map +1 -1
- package/dist/helpers/canonSpec.d.ts +12 -0
- package/dist/helpers/canonSpec.js +259 -0
- package/dist/helpers/canonSpec.js.map +1 -0
- package/dist/helpers/optspec.d.ts +4 -0
- package/dist/helpers/optspec.js +152 -0
- package/dist/helpers/optspec.js.map +1 -0
- package/dist/sdkgen.d.ts +3 -1
- package/dist/sdkgen.js +12 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/model/sdkgen.aon +125 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/audit.aon +15 -0
- package/project/.sdk/model/feature/cache.aon +13 -0
- package/project/.sdk/model/feature/clienttrack.aon +21 -0
- package/project/.sdk/model/feature/cost.aon +15 -0
- package/project/.sdk/model/feature/debug.aon +15 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/model/feature/idempotency.aon +13 -0
- package/project/.sdk/model/feature/log.aon +13 -0
- package/project/.sdk/model/feature/metrics.aon +13 -0
- package/project/.sdk/model/feature/netsim.aon +14 -0
- package/project/.sdk/model/feature/paging.aon +15 -0
- package/project/.sdk/model/feature/proxy.aon +13 -0
- package/project/.sdk/model/feature/ratelimit.aon +15 -0
- package/project/.sdk/model/feature/retry.aon +15 -0
- package/project/.sdk/model/feature/streaming.aon +15 -0
- package/project/.sdk/model/feature/telemetry.aon +19 -0
- package/project/.sdk/model/feature/test.aon +13 -0
- package/project/.sdk/model/feature/timeout.aon +15 -0
- package/project/.sdk/model/feature/validate.aon +73 -0
- package/project/.sdk/model/target/js.aon +1 -1
- package/project/.sdk/model/target/ts.aon +1 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +2 -0
- package/project/.sdk/src/cmp/js/Schema_js.ts +66 -0
- package/project/.sdk/src/cmp/scala/Main_scala.ts +30 -0
- package/project/.sdk/src/cmp/ts/Main_ts.ts +2 -0
- package/project/.sdk/src/cmp/ts/Schema_ts.ts +66 -0
- package/project/.sdk/tm/js/src/feature/validate/ValidateFeature.js +296 -0
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +26 -61
- package/project/.sdk/tm/ts/src/feature/validate/ValidateFeature.ts +298 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +24 -61
- package/project/sdkgen-package.json +3 -2
- package/src/cmp/AgentGuideContent.ts +30 -0
- package/src/cmp/FeatureDocs.ts +39 -0
- package/src/cmp/ReadmeRefFeatures.ts +24 -13
- package/src/helpers/applicability.ts +8 -0
- package/src/helpers/canonSpec.ts +301 -0
- package/src/helpers/optspec.ts +180 -0
- package/src/sdkgen.ts +9 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Content,
|
|
3
|
+
File,
|
|
4
|
+
cmp,
|
|
5
|
+
entitySpecMap,
|
|
6
|
+
optionSpec,
|
|
7
|
+
} from '@voxgig/sdkgen'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Model,
|
|
12
|
+
} from '@voxgig/apidef'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// THE GENERATED SCHEMA MODULE: the model's schemas, as data the SDK can run.
|
|
16
|
+
//
|
|
17
|
+
// Two exports, both struct.validate specs:
|
|
18
|
+
//
|
|
19
|
+
// OPTSPEC what makeOptions validates the caller's options against —
|
|
20
|
+
// the standard options from `main.kit.optspec` plus one entry
|
|
21
|
+
// per feature this target carries, built by helpers/optspec.
|
|
22
|
+
//
|
|
23
|
+
// ENTITYSPEC per entity, `{ data, op: { <opname> } }` — the record shape
|
|
24
|
+
// and each operation's request shape, mapped from the field
|
|
25
|
+
// type sentinels the model already carries (helpers/canonSpec).
|
|
26
|
+
// Emitted as an EMPTY MAP unless the `validate` feature is
|
|
27
|
+
// active, because nothing else reads it and every byte here is
|
|
28
|
+
// a byte in the consumer's package.
|
|
29
|
+
//
|
|
30
|
+
// A JSON OBJECT LITERAL, not a parsed string: JSON is a subset of the
|
|
31
|
+
// language's own literal syntax, so the values land as data with no parse
|
|
32
|
+
// step, and the backticks the sentinels carry (`\`$STRING\``) survive because
|
|
33
|
+
// JSON.stringify quotes and escapes them. The config emitter has to choose
|
|
34
|
+
// between a literal and a parsed blob because the config can be megabytes;
|
|
35
|
+
// the spec is bounded by the model's field count, so it does not.
|
|
36
|
+
const Schema = cmp(async function Schema(props: any) {
|
|
37
|
+
const ctx$ = props.ctx$
|
|
38
|
+
const target = props.target
|
|
39
|
+
|
|
40
|
+
const model: Model = ctx$.model
|
|
41
|
+
|
|
42
|
+
const optspec = optionSpec(model, target.name)
|
|
43
|
+
const entityspec = entitySpecMap(model, target.name) || {}
|
|
44
|
+
|
|
45
|
+
File({ name: 'Schema.' + target.ext }, () => {
|
|
46
|
+
Content(`// ${model.const.Name} ${target.Name} SDK: generated schemas. Do not edit.
|
|
47
|
+
//
|
|
48
|
+
// Generated from the model: \`main.kit.optspec\` and each feature's
|
|
49
|
+
// \`config.options\` for OPTSPEC; entity \`fields[].type\` for ENTITYSPEC.
|
|
50
|
+
|
|
51
|
+
const OPTSPEC = ${JSON.stringify(optspec, null, 2)}
|
|
52
|
+
|
|
53
|
+
const ENTITYSPEC = ${JSON.stringify(entityspec, null, 2)}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
OPTSPEC,
|
|
57
|
+
ENTITYSPEC,
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
Schema
|
|
66
|
+
}
|
|
@@ -86,6 +86,35 @@ const Main = cmp(async function Main(props: any) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// THE FEATURE THE MODEL NEVER MENTIONS AT ALL.
|
|
90
|
+
//
|
|
91
|
+
// inactivePluginExcludes above walks the model's DECLARED features and skips
|
|
92
|
+
// the active ones, which covers `secrets: { active: false }`. It cannot cover
|
|
93
|
+
// the case that actually shipped: a model that does not mention secrets
|
|
94
|
+
// anywhere. Then `main.kit.feature` has no `secrets` key, the loop never sees
|
|
95
|
+
// it, nothing is excluded, and all nine provider clients are copied and
|
|
96
|
+
// compiled — which is what `vocabulary.off.notrimmed` reports, and it failed
|
|
97
|
+
// on 12 of the 68 cedar SDKs.
|
|
98
|
+
//
|
|
99
|
+
// PRECISE, NOT THE WHOLE TREE. The first cut of this excluded
|
|
100
|
+
// `feature/<inactive>/` wholesale and broke the build: SecretsFeature.scala
|
|
101
|
+
// ships to every scala SDK regardless (this target's feature trim is off) and
|
|
102
|
+
// imports `com.voxgig.sekreto` and `voxgig.plugin`, so removing the vendored
|
|
103
|
+
// cores left it uncompilable — "value voxgig is not a member of com".
|
|
104
|
+
//
|
|
105
|
+
// What may go is exactly the nine provider clients under sekreto/plugins.
|
|
106
|
+
// Httpjson.scala and Sigv4.scala live there too and must STAY: both belong to
|
|
107
|
+
// no group and are shared, which model/feature/secrets.aon documents at
|
|
108
|
+
// length after a cloud-only trim once deleted Sigv4 and scalac failed with
|
|
109
|
+
// four "Not found: uriescape".
|
|
110
|
+
const SHARED_SEKRETO_PLUGINS = ['Httpjson.scala', 'Sigv4.scala']
|
|
111
|
+
const pluginDirExcludes: RegExp[] = []
|
|
112
|
+
if (null == (feature as any).secrets) {
|
|
113
|
+
pluginDirExcludes.push(new RegExp(
|
|
114
|
+
'(^|/)feature/secrets/sekreto/plugins/(?!' +
|
|
115
|
+
SHARED_SEKRETO_PLUGINS.map((f) => esc(f)).join('|') + ')[^/]+$'))
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
Package({ target })
|
|
90
119
|
|
|
91
120
|
Gitignore({})
|
|
@@ -119,6 +148,7 @@ const Main = cmp(async function Main(props: any) {
|
|
|
119
148
|
TEST_CONTROL_EXCLUDE,
|
|
120
149
|
...pluginExcludes(model),
|
|
121
150
|
...inactivePluginExcludes,
|
|
151
|
+
...pluginDirExcludes,
|
|
122
152
|
],
|
|
123
153
|
replace: {
|
|
124
154
|
...props.ctx$.stdrep,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
import { Package } from './Package_ts'
|
|
26
26
|
import { Config } from './Config_ts'
|
|
27
|
+
import { Schema } from './Schema_ts'
|
|
27
28
|
import { Gitignore } from './Gitignore_ts'
|
|
28
29
|
import { MainEntity } from './MainEntity_ts'
|
|
29
30
|
import { EntityBase } from './EntityBase_ts'
|
|
@@ -192,6 +193,7 @@ if (fres instanceof Promise) { await fres }
|
|
|
192
193
|
})
|
|
193
194
|
|
|
194
195
|
Config({ target })
|
|
196
|
+
Schema({ target })
|
|
195
197
|
EntityBase({ target })
|
|
196
198
|
EntityTypes({ target })
|
|
197
199
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Content,
|
|
3
|
+
File,
|
|
4
|
+
cmp,
|
|
5
|
+
entitySpecMap,
|
|
6
|
+
optionSpec,
|
|
7
|
+
} from '@voxgig/sdkgen'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Model,
|
|
12
|
+
} from '@voxgig/apidef'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// THE GENERATED SCHEMA MODULE: the model's schemas, as data the SDK can run.
|
|
16
|
+
//
|
|
17
|
+
// Two exports, both struct.validate specs:
|
|
18
|
+
//
|
|
19
|
+
// OPTSPEC what makeOptions validates the caller's options against —
|
|
20
|
+
// the standard options from `main.kit.optspec` plus one entry
|
|
21
|
+
// per feature this target carries, built by helpers/optspec.
|
|
22
|
+
//
|
|
23
|
+
// ENTITYSPEC per entity, `{ data, op: { <opname> } }` — the record shape
|
|
24
|
+
// and each operation's request shape, mapped from the field
|
|
25
|
+
// type sentinels the model already carries (helpers/canonSpec).
|
|
26
|
+
// Emitted as an EMPTY MAP unless the `validate` feature is
|
|
27
|
+
// active, because nothing else reads it and every byte here is
|
|
28
|
+
// a byte in the consumer's package.
|
|
29
|
+
//
|
|
30
|
+
// A JSON OBJECT LITERAL, not a parsed string: JSON is a subset of the
|
|
31
|
+
// language's own literal syntax, so the values land as data with no parse
|
|
32
|
+
// step, and the backticks the sentinels carry (`\`$STRING\``) survive because
|
|
33
|
+
// JSON.stringify quotes and escapes them. The config emitter has to choose
|
|
34
|
+
// between a literal and a parsed blob because the config can be megabytes;
|
|
35
|
+
// the spec is bounded by the model's field count, so it does not.
|
|
36
|
+
const Schema = cmp(async function Schema(props: any) {
|
|
37
|
+
const ctx$ = props.ctx$
|
|
38
|
+
const target = props.target
|
|
39
|
+
|
|
40
|
+
const model: Model = ctx$.model
|
|
41
|
+
|
|
42
|
+
const optspec = optionSpec(model, target.name)
|
|
43
|
+
const entityspec = entitySpecMap(model, target.name) || {}
|
|
44
|
+
|
|
45
|
+
File({ name: 'Schema.' + target.ext }, () => {
|
|
46
|
+
Content(`// ${model.const.Name} ${target.Name} SDK: generated schemas. Do not edit.
|
|
47
|
+
//
|
|
48
|
+
// Generated from the model: \`main.kit.optspec\` and each feature's
|
|
49
|
+
// \`config.options\` for OPTSPEC; entity \`fields[].type\` for ENTITYSPEC.
|
|
50
|
+
|
|
51
|
+
const OPTSPEC = ${JSON.stringify(optspec, null, 2)}
|
|
52
|
+
|
|
53
|
+
const ENTITYSPEC = ${JSON.stringify(entityspec, null, 2)}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
OPTSPEC,
|
|
57
|
+
ENTITYSPEC,
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
Schema
|
|
66
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
|
|
2
|
+
const { ENTITYSPEC } = require('../../Schema')
|
|
3
|
+
|
|
4
|
+
const { BaseFeature } = require('../base/BaseFeature')
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Payload validation against the model's own field types.
|
|
8
|
+
//
|
|
9
|
+
// The specs are NOT written here and not written in the model either: every
|
|
10
|
+
// entity field already carries a canonical type sentinel (`$STRING`,
|
|
11
|
+
// `$INTEGER`, the `$ONE` union for an OpenAPI multi-type), which is the same
|
|
12
|
+
// vocabulary struct.validate speaks. The generator maps them once
|
|
13
|
+
// (helpers/canonSpec) and emits `ENTITYSPEC` beside this file, so a field
|
|
14
|
+
// whose type changes in the API spec changes what this feature enforces with
|
|
15
|
+
// no edit anywhere.
|
|
16
|
+
//
|
|
17
|
+
// WHAT IS CHECKED
|
|
18
|
+
// outbound (PreSpec) the payload the caller asked to send, against
|
|
19
|
+
// `spec.op[<opname>]` — the operation's request shape,
|
|
20
|
+
// which is the SAME partiality policy that decides what
|
|
21
|
+
// the generated `<Name>CreateData` type requires.
|
|
22
|
+
// inbound (PreDone) each record the operation returned, against
|
|
23
|
+
// `spec.data` — the entity's own field types.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IS NOT. The model carries no array element types, no nested object
|
|
26
|
+
// schemas, no enums, formats or bounds (see canonSpec's note), so this checks
|
|
27
|
+
// the shape the model knows and nothing more. It is a guard against the
|
|
28
|
+
// mistakes the model CAN see — a number where a string belongs, a required
|
|
29
|
+
// field left out, a misspelled key under `strict` — not a substitute for the
|
|
30
|
+
// server's own validation.
|
|
31
|
+
class ValidateFeature extends BaseFeature {
|
|
32
|
+
version = '0.0.1'
|
|
33
|
+
name = 'validate'
|
|
34
|
+
active = true
|
|
35
|
+
|
|
36
|
+
_client
|
|
37
|
+
_options = {}
|
|
38
|
+
_spec = {}
|
|
39
|
+
|
|
40
|
+
_request = true
|
|
41
|
+
_response = false
|
|
42
|
+
_mode = 'throw'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
init(ctx, options) {
|
|
46
|
+
this._client = ctx.client
|
|
47
|
+
this._options = options || {}
|
|
48
|
+
this.active = options.active
|
|
49
|
+
|
|
50
|
+
// DEFAULTS ARE APPLIED HERE, not by the option spec. The model's
|
|
51
|
+
// `config.options` documents them and types them; it does not inject
|
|
52
|
+
// them, because each feature entry in the spec is optional and struct
|
|
53
|
+
// fills in nothing through an optional union. So every feature resolves
|
|
54
|
+
// its own — and a `mode` left undefined here once meant `'throw' !==
|
|
55
|
+
// undefined`, which silently turned every rejection into a no-op.
|
|
56
|
+
this._request = false !== this._options.request
|
|
57
|
+
this._response = true === this._options.response
|
|
58
|
+
|
|
59
|
+
// FAIL CLOSED. Only the exact string 'report' selects report mode, so a
|
|
60
|
+
// typo (`mode: 'thow'`) still rejects rather than silently turning
|
|
61
|
+
// enforcement off — the failure nobody would notice. The option spec
|
|
62
|
+
// rejects the typo outright; this is what happens if it ever does not.
|
|
63
|
+
this._mode = 'report' === this._options.mode ? 'report' : 'throw'
|
|
64
|
+
|
|
65
|
+
// `strict` is applied ONCE, here, by rebuilding the spec tree without the
|
|
66
|
+
// `$OPEN` markers — rather than per call, which would clone a spec for
|
|
67
|
+
// every request an SDK ever makes.
|
|
68
|
+
this._spec = true === this._options.strict ? close(ENTITYSPEC) : ENTITYSPEC
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// Outbound. `makeSpec` short-circuits on an `ctx.out.spec` that is already
|
|
73
|
+
// set, so assigning the error here rejects the operation before the request
|
|
74
|
+
// is built — the same seam rbac uses one stage earlier.
|
|
75
|
+
PreSpec(ctx) {
|
|
76
|
+
if (!this.active || !this._request) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const opname = (ctx.op && ctx.op.name) || ''
|
|
81
|
+
const spec = this._entitySpec(ctx)
|
|
82
|
+
const opspec = spec && spec.op ? spec.op[opname] : null
|
|
83
|
+
|
|
84
|
+
if (null == opspec) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const errs = this._check(ctx, this._payload(ctx, opname), opspec, 'request')
|
|
89
|
+
if (0 === errs.length || 'report' === this._mode) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const err = ctx.error('validate_failed',
|
|
94
|
+
'Invalid ' + opname + ' request for entity "' + entname(ctx) + '": ' +
|
|
95
|
+
errs.join('; '))
|
|
96
|
+
ctx.out.spec = err
|
|
97
|
+
return err
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// Inbound. PreDone rather than PreResult: the records are extracted from
|
|
102
|
+
// the response body by `makeResult`, which runs between the two, so at
|
|
103
|
+
// PreResult there is nothing to check but the envelope.
|
|
104
|
+
//
|
|
105
|
+
// HOOK ORDER MATTERS HERE, and the default order is not the one you want.
|
|
106
|
+
// PreDone hooks fire in feature ADD order, which defaults to `test` first
|
|
107
|
+
// and then names sorted — and `validate` sorts last, after `audit`,
|
|
108
|
+
// `cost`, `debug`, `metrics` and `telemetry`. Those observers therefore
|
|
109
|
+
// record the operation as a success before this hook has looked at it.
|
|
110
|
+
// Activating features as an ORDERED ARRAY fixes it:
|
|
111
|
+
//
|
|
112
|
+
// feature: [{ name: 'validate', active: true, response: true },
|
|
113
|
+
// { name: 'metrics', active: true }]
|
|
114
|
+
//
|
|
115
|
+
// What this feature can fix from here, it does: the result is marked
|
|
116
|
+
// failed and its records are cleared, so the entity absorbs nothing.
|
|
117
|
+
PreDone(ctx) {
|
|
118
|
+
if (!this.active || !this._response) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const spec = this._entitySpec(ctx)
|
|
123
|
+
if (null == spec || null == spec.data) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resdata = ctx.result && ctx.result.resdata
|
|
128
|
+
if (null == resdata) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A list op returns many records and a load returns one; both are checked
|
|
133
|
+
// against the same record spec, because they are the same entity.
|
|
134
|
+
const records = Array.isArray(resdata) ? resdata : [resdata]
|
|
135
|
+
const errs = []
|
|
136
|
+
for (const record of records) {
|
|
137
|
+
if (null == record) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A NON-OBJECT IS A FAILURE, not something to skip. A load that
|
|
142
|
+
// answered `42` where the entity's spec wants a record used to pass
|
|
143
|
+
// this feature silently, which is the one outcome a validator must
|
|
144
|
+
// never produce. struct rejects it with the field it could not find.
|
|
145
|
+
for (const e of this._check(ctx, unwrap(record), spec.data, 'response')) {
|
|
146
|
+
errs.push(e)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (0 === errs.length || 'report' === this._mode) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const err = ctx.error('validate_failed',
|
|
155
|
+
'Invalid response for entity "' + entname(ctx) + '": ' + errs.join('; '))
|
|
156
|
+
|
|
157
|
+
// BOTH, and `ok` is the load-bearing half: `done` returns `resdata`
|
|
158
|
+
// whenever `result.ok` is true and never looks at `err`, so setting the
|
|
159
|
+
// error alone handed the caller the very records that failed the spec.
|
|
160
|
+
ctx.result.ok = false
|
|
161
|
+
ctx.result.err = err
|
|
162
|
+
|
|
163
|
+
// AND THE DATA GOES. The load/update fragments copy `result.resdata`
|
|
164
|
+
// into the entity's own state on any non-null value, BEFORE `done`
|
|
165
|
+
// raises — so rejecting the operation while leaving the records in place
|
|
166
|
+
// left the caller holding an entity populated from a payload this
|
|
167
|
+
// feature had just declared invalid. Clearing it is the only half of
|
|
168
|
+
// that this feature owns; see the note on hook order below.
|
|
169
|
+
ctx.result.resdata = undefined
|
|
170
|
+
|
|
171
|
+
return err
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// The payload an operation is about to send.
|
|
176
|
+
//
|
|
177
|
+
// TWO SLOTS, AND THE OP PICKS. A body op (create/update/patch) carries the
|
|
178
|
+
// caller's argument in `reqdata` over the entity's `data`; a match op
|
|
179
|
+
// (load/list/remove) carries it in `reqmatch` over `match`. That is what
|
|
180
|
+
// the Entity*Op fragments pass to makeContext and what makePoint reads
|
|
181
|
+
// (`getprop(ctx, 'req' + op.input)`) — so reading `reqdata` for every op
|
|
182
|
+
// checked a `load({ id })` against the entity's STALE stored match and
|
|
183
|
+
// rejected it for the id the caller had just supplied.
|
|
184
|
+
_payload(ctx, opname) {
|
|
185
|
+
const body = 'create' === opname || 'update' === opname || 'patch' === opname
|
|
186
|
+
|
|
187
|
+
const base = body ? ctx.data : ctx.match
|
|
188
|
+
const req = body ? ctx.reqdata : ctx.reqmatch
|
|
189
|
+
|
|
190
|
+
const out = { ...(base || {}), ...(req || {}) }
|
|
191
|
+
|
|
192
|
+
// `$action` SELECTS A CUSTOM ENDPOINT; it is not a field of the record.
|
|
193
|
+
// makePoint reads it off this same argument and the request transformer
|
|
194
|
+
// drops it before the body is built, so a spec built from the API's own
|
|
195
|
+
// fields will never name it — and under `strict` every custom-action
|
|
196
|
+
// call would be rejected for the one key that made it reachable.
|
|
197
|
+
delete out.$action
|
|
198
|
+
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
_entitySpec(ctx) {
|
|
204
|
+
return this._spec[entname(ctx)]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
// One validate call. Errors are COLLECTED, never thrown: struct throws on
|
|
209
|
+
// the first failure unless given an `errs` array, and a caller fixing a
|
|
210
|
+
// payload wants every problem with it, not the first one.
|
|
211
|
+
_check(ctx, data, spec, direction) {
|
|
212
|
+
const struct = ctx.utility.struct
|
|
213
|
+
const errs = []
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
struct.validate(data, spec, { errs })
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
// A spec this port cannot run at all (rather than a payload that fails
|
|
220
|
+
// it) must not take the operation down with it: report it like any
|
|
221
|
+
// other failure and let `mode` decide.
|
|
222
|
+
errs.push(e && e.message ? e.message : String(e))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (0 < errs.length && 'function' === typeof this._options.onInvalid) {
|
|
226
|
+
try {
|
|
227
|
+
this._options.onInvalid({
|
|
228
|
+
entity: entname(ctx),
|
|
229
|
+
op: (ctx.op && ctx.op.name) || '',
|
|
230
|
+
direction,
|
|
231
|
+
errs,
|
|
232
|
+
data,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
catch (_e) { }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return errs
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
// A RESULT RECORD AS DATA.
|
|
244
|
+
//
|
|
245
|
+
// `makeResult` turns every record of a LIST into an entity instance
|
|
246
|
+
// (`entity.make()` then `ent.data(entry)`), so what reaches PreDone for a
|
|
247
|
+
// list is wrappers, not records — and a wrapper checked against a field spec
|
|
248
|
+
// fails on every required field while its actual data goes unchecked. A load
|
|
249
|
+
// returns the record itself, so this has to handle both.
|
|
250
|
+
function unwrap(record) {
|
|
251
|
+
if (null != record && 'function' === typeof record.data) {
|
|
252
|
+
const data = record.data()
|
|
253
|
+
if (null != data) {
|
|
254
|
+
return data
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return record
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
function entname(ctx) {
|
|
262
|
+
return (ctx.entity && ctx.entity.name) || (ctx.op && ctx.op.entity) || ''
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
// The spec tree with every `$OPEN` marker removed, so an undeclared key is an
|
|
267
|
+
// error rather than a pass. Rebuilt rather than mutated: ENTITYSPEC is a
|
|
268
|
+
// module constant shared by every client in the process.
|
|
269
|
+
function close(node) {
|
|
270
|
+
if (Array.isArray(node)) {
|
|
271
|
+
return node.map((n) => close(n))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (null == node || 'object' !== typeof node) {
|
|
275
|
+
return node
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const out = {}
|
|
279
|
+
for (const key of Object.keys(node)) {
|
|
280
|
+
if (OPEN === key) {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
out[key] = close(node[key])
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
// Built rather than written, so the backticks cannot be lost in an edit.
|
|
291
|
+
const OPEN = String.fromCharCode(96) + '$OPEN' + String.fromCharCode(96)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
ValidateFeature
|
|
296
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
|
|
2
|
+
const { OPTSPEC } = require('../Schema')
|
|
3
|
+
|
|
4
|
+
|
|
2
5
|
function makeOptions(ctx) {
|
|
3
6
|
const utility = ctx.utility
|
|
4
7
|
const options = ctx.options
|
|
@@ -57,67 +60,19 @@ function makeOptions(ctx) {
|
|
|
57
60
|
let config = ctx.config || {}
|
|
58
61
|
let cfgopts = config.options || {}
|
|
59
62
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
allow: {
|
|
75
|
-
method: 'GET,PUT,POST,PATCH,DELETE,OPTIONS',
|
|
76
|
-
op: 'create,update,load,list,remove,command,direct,graphql'
|
|
77
|
-
},
|
|
78
|
-
entity: {
|
|
79
|
-
'`$CHILD`': {
|
|
80
|
-
'`$OPEN`': true,
|
|
81
|
-
active: false,
|
|
82
|
-
alias: {}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
feature: {
|
|
86
|
-
'`$CHILD`': {
|
|
87
|
-
'`$OPEN`': true,
|
|
88
|
-
active: false,
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
utility: {},
|
|
92
|
-
// Feature INSTANCES supplied at construction (the station adopt
|
|
93
|
-
// path): consumed by the constructor's featureAdd loop, so they are
|
|
94
|
-
// class instances, not data - `$ANY` accepts them verbatim. Without
|
|
95
|
-
// this entry the seam is dead: the constructor reads
|
|
96
|
-
// options.extend, but validate rejected the key.
|
|
97
|
-
extend: '`$ANY`',
|
|
98
|
-
system: {
|
|
99
|
-
fetch: undefined
|
|
100
|
-
},
|
|
101
|
-
test: {
|
|
102
|
-
active: false,
|
|
103
|
-
entity: {
|
|
104
|
-
'`$OPEN`': true,
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
clean: {
|
|
108
|
-
keys: 'key,token,id'
|
|
109
|
-
},
|
|
110
|
-
// Server-variable values for a templated base URL (OpenAPI server
|
|
111
|
-
// variables): `{name}` placeholders in `base` are substituted from
|
|
112
|
-
// this map at construction. Spec defaults arrive via the generated
|
|
113
|
-
// Config; user values override them.
|
|
114
|
-
server: {
|
|
115
|
-
'`$CHILD`': ''
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// JavaScript specific option values.
|
|
120
|
-
optspec.system.fetch = opts.system?.fetch || global.fetch
|
|
63
|
+
// THE OPTION SPEC IS GENERATED, NOT WRITTEN HERE.
|
|
64
|
+
//
|
|
65
|
+
// `Spec.OPTSPEC` is built from the model: `main.kit.optspec` for the
|
|
66
|
+
// standard options, plus one entry per feature this target carries, taken
|
|
67
|
+
// from that feature's own `config.options` / `config.optspec`. Editing this
|
|
68
|
+
// file to add an option would put it back where it was — one of twenty
|
|
69
|
+
// hand-maintained copies of a schema nothing cross-checked — so add it to
|
|
70
|
+
// the model instead and every ported target validates it.
|
|
71
|
+
//
|
|
72
|
+
// NOT MUTATED. It is a module-level constant shared by every client this
|
|
73
|
+
// process constructs; the platform default below is applied to the RESULT,
|
|
74
|
+
// never to the spec.
|
|
75
|
+
const optspec = OPTSPEC
|
|
121
76
|
|
|
122
77
|
// Clone the config side before merging: `config` is a module-level
|
|
123
78
|
// singleton in ts/js, and merge would otherwise use its nested maps as
|
|
@@ -127,6 +82,16 @@ function makeOptions(ctx) {
|
|
|
127
82
|
|
|
128
83
|
opts = validate(opts, optspec)
|
|
129
84
|
|
|
85
|
+
// The platform fetch, supplied AFTER validate rather than as a spec
|
|
86
|
+
// default. `system.fetch` is declared `$ANY`, which passes a caller's own
|
|
87
|
+
// fetch through untouched but inserts nothing when the key is absent — and
|
|
88
|
+
// the spec is shared, so writing the default into it (as this did while the
|
|
89
|
+
// spec was a per-call literal) would hand one client's fetch to the next.
|
|
90
|
+
opts.system = opts.system || {}
|
|
91
|
+
if (null == opts.system.fetch) {
|
|
92
|
+
opts.system.fetch = global.fetch
|
|
93
|
+
}
|
|
94
|
+
|
|
130
95
|
// Restore the suppression the optspec default would otherwise erase.
|
|
131
96
|
if (authsuppressed) {
|
|
132
97
|
opts.auth = null
|