@voxgig/sdkgen 4.15.0 → 4.17.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/README.md +8 -0
- package/bin/voxgig-sdkgen +2 -2
- package/dist/action/action.js +2 -2
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.js +9 -8
- package/dist/action/check.js.map +1 -1
- package/dist/action/dispatch.js +4 -4
- package/dist/action/dispatch.js.map +1 -1
- package/dist/action/doctor.js +16 -15
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.d.ts +4 -0
- package/dist/action/{docs.js → edition.js} +57 -54
- package/dist/action/edition.js.map +1 -0
- package/dist/action/kind.d.ts +1 -2
- package/dist/action/kind.js +22 -38
- package/dist/action/kind.js.map +1 -1
- package/dist/action/package.js +17 -16
- package/dist/action/package.js.map +1 -1
- package/dist/action/resolve.d.ts +3 -1
- package/dist/action/resolve.js +29 -11
- package/dist/action/resolve.js.map +1 -1
- package/dist/admin/status.d.ts +43 -0
- package/dist/admin/status.js +161 -0
- package/dist/admin/status.js.map +1 -0
- package/dist/cmp/Test.js +11 -0
- package/dist/cmp/Test.js.map +1 -1
- package/dist/helpers/kindCollection.d.ts +1 -0
- package/dist/helpers/kindCollection.js +14 -0
- package/dist/helpers/kindCollection.js.map +1 -0
- package/dist/helpers/modelcheck.d.ts +1 -1
- package/dist/helpers/modelcheck.js +13 -3
- package/dist/helpers/modelcheck.js.map +1 -1
- package/dist/helpers/shipped.js +2 -3
- package/dist/helpers/shipped.js.map +1 -1
- package/dist/helpers/testPolicy.d.ts +1 -1
- package/dist/helpers/testPolicy.js +6 -37
- package/dist/helpers/testPolicy.js.map +1 -1
- package/dist/sdkgen.d.ts +7 -1
- package/dist/sdkgen.js +146 -89
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.js +9 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +5 -52
- package/package.json +1 -1
- package/project/.sdk/src/cmp/go/Config_go.ts +4 -4
- package/project/.sdk/src/cmp/go/utility_go.ts +10 -2
- package/project/.sdk/src/cmp/js/Package_js.ts +5 -1
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +11 -7
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +16 -2
- package/project/.sdk/src/cmp/js/TestLive_js.ts +32 -0
- package/project/.sdk/src/cmp/js/Test_js.ts +2 -0
- package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +1 -1
- package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +4 -2
- package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +30 -9
- package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +8 -3
- package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +5 -4
- package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +2 -2
- package/project/.sdk/src/cmp/ts/Package_ts.ts +5 -1
- package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +5 -0
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -11
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +13 -16
- package/project/.sdk/src/cmp/ts/TestLive_ts.ts +32 -0
- package/project/.sdk/src/cmp/ts/Test_ts.ts +2 -0
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +1 -0
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +2 -0
- package/project/.sdk/tm/csharp/test/feature/secrets/SecretsFeatureTest.cs +9 -2
- package/project/.sdk/tm/js/test/live-contract.js +183 -0
- package/project/.sdk/tm/js/test/live-entity.js +169 -0
- package/project/.sdk/tm/js/test/live-runner.js +129 -0
- package/project/.sdk/tm/js/test/live-scenarios.js +142 -0
- package/project/.sdk/tm/js/test/utility.js +84 -0
- package/project/.sdk/tm/ts/test/live-contract.ts +104 -0
- package/project/.sdk/tm/ts/test/live-entity.ts +142 -0
- package/project/.sdk/tm/ts/test/live-runner.ts +166 -0
- package/project/.sdk/tm/ts/test/live-scenarios.ts +112 -0
- package/project/.sdk/tm/ts/test/utility.ts +1 -2
- package/project/sdkgen-package.json +1 -1
- package/src/action/action.ts +2 -2
- package/src/action/check.ts +3 -2
- package/src/action/dispatch.ts +4 -4
- package/src/action/doctor.ts +10 -9
- package/src/action/{docs.ts → edition.ts} +56 -53
- package/src/action/kind.ts +12 -33
- package/src/action/package.ts +4 -3
- package/src/action/resolve.ts +26 -2
- package/src/admin/status.ts +124 -0
- package/src/cmp/Test.ts +13 -1
- package/src/helpers/kindCollection.ts +11 -0
- package/src/helpers/modelcheck.ts +10 -3
- package/src/helpers/shipped.ts +2 -3
- package/src/helpers/testPolicy.ts +7 -45
- package/src/sdkgen.ts +189 -93
- package/src/utility.ts +8 -2
- package/dist/action/docs.d.ts +0 -4
- package/dist/action/docs.js.map +0 -1
- package/dist/cmp/Docs.d.ts +0 -4
- package/dist/cmp/Docs.js +0 -98
- package/dist/cmp/Docs.js.map +0 -1
- package/dist/cmp/ExternalDocs.d.ts +0 -2
- package/dist/cmp/ExternalDocs.js +0 -43
- package/dist/cmp/ExternalDocs.js.map +0 -1
- package/src/cmp/Docs.ts +0 -125
- package/src/cmp/ExternalDocs.ts +0 -54
package/model/sdkgen.aon
CHANGED
|
@@ -28,21 +28,10 @@ main: kit: config: repr: *'auto' | 'literal' | 'data'
|
|
|
28
28
|
main: kit: test: {
|
|
29
29
|
|
|
30
30
|
live: {
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
# early return, not a failure. Synthetic IDs 4xx constantly and list
|
|
36
|
-
# response shapes vary wildly, so asserting would mean permanent red.
|
|
37
|
-
#
|
|
38
|
-
# true: live assertions match the offline ones — a non-2xx FAILS, and a
|
|
39
|
-
# missing idmap entry fails rather than skipping. Set this when the
|
|
40
|
-
# project OWNS the server it tests against; otherwise a live suite passes
|
|
41
|
-
# with nothing listening on the port, which is what it did.
|
|
42
|
-
#
|
|
43
|
-
# Overridable per target:
|
|
44
|
-
# main: kit: target: go: test: live: strict: true
|
|
45
|
-
strict: *false | boolean
|
|
31
|
+
# Assert live request outcomes, while continuing independent work.
|
|
32
|
+
# Explicit false retains the legacy exploratory direct-test policy.
|
|
33
|
+
# Per-target overrides remain available for migration.
|
|
34
|
+
strict: *true | boolean
|
|
46
35
|
|
|
47
36
|
# The base URL a live run points at. '' derives it from the spec's first
|
|
48
37
|
# declared server, but ONLY when that is a loopback address.
|
|
@@ -585,40 +574,4 @@ main: kit: option: &: {
|
|
|
585
574
|
|
|
586
575
|
|
|
587
576
|
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
# A docs item is a generation target whose destination is a DOCUMENTATION
|
|
591
|
-
# SYSTEM rather than a language: a static site, a developer-portal catalogue,
|
|
592
|
-
# a hosted service's config. sdkgen ships the kind; the items live in
|
|
593
|
-
# packages, so this spread declares only what EVERY destination needs and
|
|
594
|
-
# leaves the rest to the item's own model file (an unknown key unifies fine).
|
|
595
|
-
#
|
|
596
|
-
# Deliberately NOT copied from the target spread: `ext`, `comment.line` and
|
|
597
|
-
# `module.name` are required there and have no honest value for a site
|
|
598
|
-
# emitting .md, .yml, .css and .svg at once. `title` is defaulted rather than
|
|
599
|
-
# required, so an author is never forced to invent one to make the model
|
|
600
|
-
# compile — the `module.name` trap of §10.1.
|
|
601
|
-
main: kit: docs: &: {
|
|
602
|
-
name: key()
|
|
603
|
-
active: *true | boolean
|
|
604
|
-
title: *'' | string
|
|
605
|
-
|
|
606
|
-
# Provenance — see the note on `main: kit: target: &:` above. Written by
|
|
607
|
-
# `docs add`, read by doctor, `package list` and `package update`.
|
|
608
|
-
base: *'' | string
|
|
609
|
-
origname: *'' | string
|
|
610
|
-
package: *'' | string
|
|
611
|
-
|
|
612
|
-
# Where the item generates. '' is in-tree, at `<sdk-repo>/<name>/`;
|
|
613
|
-
# anything else is its own generate() pass rooted there, exactly as for a
|
|
614
|
-
# target (see explanation/out-of-tree-targets). Out of tree is the normal
|
|
615
|
-
# case for a docs item: a documentation site usually has its own repo, and
|
|
616
|
-
# in-tree it would collide with the SDK repo's own README and workflows.
|
|
617
|
-
output: {
|
|
618
|
-
path: *'' | string
|
|
619
|
-
repo: *'' | string
|
|
620
|
-
create: *true | boolean
|
|
621
|
-
adopt: *false | boolean
|
|
622
|
-
sdkrel: *'' | string
|
|
623
|
-
}
|
|
624
|
-
}
|
|
577
|
+
# Documentation editions extend main.kit.doc through @voxgig/docgen/model/docgen.aon.
|
package/package.json
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
32
|
formatGoMap,
|
|
33
|
+
formatGoString,
|
|
33
34
|
goFeatureName,
|
|
34
35
|
} from './utility_go'
|
|
35
36
|
|
|
@@ -133,9 +134,8 @@ const Config = cmp(async function Config(props: any) {
|
|
|
133
134
|
// 2.1x smaller. MakeConfig still returns the same map, so nothing
|
|
134
135
|
// downstream can tell which representation it got.
|
|
135
136
|
//
|
|
136
|
-
// JSON
|
|
137
|
-
// escapes
|
|
138
|
-
// no escaping. A raw (backtick) literal could NOT be used - the model
|
|
137
|
+
// JSON escapes work in Go interpreted strings; formatGoString also
|
|
138
|
+
// escapes BOM characters that Go forbids literally inside source files. A raw (backtick) literal could NOT be used - the model
|
|
139
139
|
// contains backticks in values like `$STRING`.
|
|
140
140
|
if (asData) {
|
|
141
141
|
Content(`package core
|
|
@@ -148,7 +148,7 @@ ${pluginImportBlock})
|
|
|
148
148
|
|
|
149
149
|
// The API model, emitted as data rather than as a composite literal: see
|
|
150
150
|
// sdkgen rung L1. Parsed by MakeConfig, and parsed once by SharedConfig.
|
|
151
|
-
const configJSON = ${
|
|
151
|
+
const configJSON = ${formatGoString(configJson)}
|
|
152
152
|
|
|
153
153
|
// json.Unmarshal decodes EVERY JSON number as float64, but the literal
|
|
154
154
|
// representation emits an integer token as an untyped constant that lands in
|
|
@@ -127,7 +127,7 @@ function formatGoMap(obj: any, indent: number = 0): string {
|
|
|
127
127
|
return 'map[string]any{}'
|
|
128
128
|
}
|
|
129
129
|
const items = entries
|
|
130
|
-
.map(([k, v]) => `${padInner}
|
|
130
|
+
.map(([k, v]) => `${padInner}${formatGoString(k)}: ${formatGoValue(v, indent + 1)}`)
|
|
131
131
|
.join(',\n')
|
|
132
132
|
return `map[string]any{\n${items},\n${pad}}`
|
|
133
133
|
}
|
|
@@ -136,12 +136,19 @@ function formatGoMap(obj: any, indent: number = 0): string {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
// Go rejects a literal BOM anywhere after the start of a source file,
|
|
140
|
+
// including inside strings. Preserve its value through an escaped literal.
|
|
141
|
+
function formatGoString(value: string): string {
|
|
142
|
+
return JSON.stringify(value).replace(/\uFEFF/g, '\\ufeff')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
139
146
|
function formatGoValue(val: any, indent: number = 0): string {
|
|
140
147
|
if (val === null || val === undefined) {
|
|
141
148
|
return 'nil'
|
|
142
149
|
}
|
|
143
150
|
if (typeof val === 'string') {
|
|
144
|
-
return
|
|
151
|
+
return formatGoString(val)
|
|
145
152
|
}
|
|
146
153
|
if (typeof val === 'number') {
|
|
147
154
|
if (Number.isInteger(val)) {
|
|
@@ -251,6 +258,7 @@ export {
|
|
|
251
258
|
clean,
|
|
252
259
|
exampleValue,
|
|
253
260
|
formatGoMap,
|
|
261
|
+
formatGoString,
|
|
254
262
|
formatGoValue,
|
|
255
263
|
goVarName,
|
|
256
264
|
projectPath,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
PUBLISHER_URL,
|
|
14
14
|
packageVersion,
|
|
15
15
|
authorInfo,
|
|
16
|
-
targetFeatures,
|
|
16
|
+
targetFeatures, envName,
|
|
17
17
|
} from '@voxgig/sdkgen'
|
|
18
18
|
|
|
19
19
|
|
|
@@ -81,6 +81,10 @@ const Package = cmp(async function Package(props: any) {
|
|
|
81
81
|
// directly (no build step), so that is the whole package.
|
|
82
82
|
files: ['src'],
|
|
83
83
|
scripts: {
|
|
84
|
+
...(Object.values(model.main.kit.entity || {}).some((e: any) => Object.values(e.op || {}).some((o: any) => (o.points || []).some((p: any) => p.contract && JSON.parse(p.contract.json).live))) ? {
|
|
85
|
+
'test:live': `${envName(model)}_TEST_LIVE=TRUE node --test test/live.test.js`,
|
|
86
|
+
} : {}),
|
|
87
|
+
|
|
84
88
|
'test': 'node --test \'test/**/*.test.js\'',
|
|
85
89
|
'test-some': 'node --experimental-test-isolation=none ' +
|
|
86
90
|
'--test-name-pattern=\"$TEST_PATTERN\" --test \'test/**/*.test.js\'',
|
|
@@ -96,6 +96,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
96
96
|
|
|
97
97
|
Slot({ name: 'directSetup' }, () => {
|
|
98
98
|
Content(`
|
|
99
|
+
function liveScenariosActive() { return ${Object.values(model.main.kit.entity || {}).some((e: any) => Object.values(e.op || {}).some((o: any) => (o.points || []).some((p: any) => p.contract && JSON.parse(p.contract.json).live)))} && process.env.${PROJECTNAME}_TEST_LIVE === 'TRUE' }
|
|
99
100
|
function directSetup(mockres) {
|
|
100
101
|
const calls = []
|
|
101
102
|
|
|
@@ -220,7 +221,7 @@ ${listParamLines}
|
|
|
220
221
|
assert(listResult.ok === true)
|
|
221
222
|
const listData = listResult.data
|
|
222
223
|
if (!Array.isArray(listData) || listData.length === 0) {
|
|
223
|
-
|
|
224
|
+
throw new Error('Live load blocked: discovery returned no usable entities')
|
|
224
225
|
}
|
|
225
226
|
params.id = listData[0].id
|
|
226
227
|
${ancestorParamLines}
|
|
@@ -234,7 +235,8 @@ ${loadParams.map((p: any, i: number) => ` ${jsProp('params', p.name)} = 'di
|
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
Content(`
|
|
237
|
-
test('direct-load-${entity.name}', async () => {
|
|
238
|
+
test('direct-load-${entity.name}', async (t) => {
|
|
239
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
238
240
|
const setup = directSetup({ id: 'direct01' })
|
|
239
241
|
const { client, calls } = setup
|
|
240
242
|
|
|
@@ -248,7 +250,7 @@ ${liveParamsBlock}
|
|
|
248
250
|
})
|
|
249
251
|
|
|
250
252
|
assert(result.ok === true)
|
|
251
|
-
assert(result.status === 200)
|
|
253
|
+
assert(setup.live ? result.status >= 200 && result.status < 300 : result.status === 200)
|
|
252
254
|
assert(null != result.data)
|
|
253
255
|
|
|
254
256
|
if (!setup.live) {
|
|
@@ -308,7 +310,8 @@ ${mockLines}
|
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
Content(`
|
|
311
|
-
test('direct-list-${entity.name}', async () => {
|
|
313
|
+
test('direct-list-${entity.name}', async (t) => {
|
|
314
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
312
315
|
const setup = directSetup([{ id: 'direct01' }, { id: 'direct02' }])
|
|
313
316
|
const { client, calls } = setup
|
|
314
317
|
|
|
@@ -320,7 +323,7 @@ ${paramsBlock}
|
|
|
320
323
|
})
|
|
321
324
|
|
|
322
325
|
assert(result.ok === true)
|
|
323
|
-
assert(result.status === 200)
|
|
326
|
+
assert(setup.live ? result.status >= 200 && result.status < 300 : result.status === 200)
|
|
324
327
|
assert(Array.isArray(result.data))
|
|
325
328
|
|
|
326
329
|
if (!setup.live) {
|
|
@@ -359,7 +362,8 @@ function generateDirectGraphqlJs(
|
|
|
359
362
|
' assert(calls[0].init.body.includes(\'direct0' + (i + 1) + '\'))\n').join('')
|
|
360
363
|
|
|
361
364
|
Content(`
|
|
362
|
-
test('direct-${opname}-${entity.name}', async () => {
|
|
365
|
+
test('direct-${opname}-${entity.name}', async (t) => {
|
|
366
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
363
367
|
const setup = directSetup()
|
|
364
368
|
const { client, calls } = setup
|
|
365
369
|
|
|
@@ -373,7 +377,7 @@ ${mockVarLines || ' // no variables'}
|
|
|
373
377
|
const result = await client.graphql(${JSON.stringify(doc)}, variables)
|
|
374
378
|
|
|
375
379
|
assert(result.ok === true)
|
|
376
|
-
assert(result.status === 200)
|
|
380
|
+
assert(setup.live ? result.status >= 200 && result.status < 300 : result.status === 200)
|
|
377
381
|
assert(null != result.data)
|
|
378
382
|
|
|
379
383
|
if (!setup.live) {
|
|
@@ -171,7 +171,14 @@ function basicSetup(extra) {
|
|
|
171
171
|
|
|
172
172
|
idmap = env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
const live = 'TRUE' === env.${PROJENVNAME}_TEST_LIVE
|
|
175
|
+
const transport = createLiveTransport()
|
|
176
|
+
if (live) {
|
|
177
|
+
const rawIds = process.env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
178
|
+
idmap = rawIds && rawIds.trim() ? JSON.parse(rawIds) : {}
|
|
179
|
+
if (!idmap || Array.isArray(idmap) || typeof idmap !== 'object') {
|
|
180
|
+
throw new Error('Live ENTID must be a JSON object')
|
|
181
|
+
}
|
|
175
182
|
client = new ${model.Name}SDK(merge([
|
|
176
183
|
// FIRST, so the generated fields below win: sdk-test-control.json's
|
|
177
184
|
// test.client.options adds to the live client, it does not redirect it.
|
|
@@ -182,7 +189,8 @@ function basicSetup(extra) {
|
|
|
182
189
|
// the last entry is undefined, and basicSetup is normally called with no
|
|
183
190
|
// argument at all - so a bare 'extra' silently discarded the apikey and
|
|
184
191
|
// server values above and handed the SDK undefined.
|
|
185
|
-
extra || {}
|
|
192
|
+
extra || {},
|
|
193
|
+
{ system: { fetch: transport.fetch } }
|
|
186
194
|
]))
|
|
187
195
|
}
|
|
188
196
|
|
|
@@ -194,6 +202,8 @@ function basicSetup(extra) {
|
|
|
194
202
|
struct,
|
|
195
203
|
data: entityData,
|
|
196
204
|
explain: 'TRUE' === env.${PROJENVNAME}_TEST_EXPLAIN,
|
|
205
|
+
live,
|
|
206
|
+
transport,
|
|
197
207
|
now: Date.now(),
|
|
198
208
|
}
|
|
199
209
|
|
|
@@ -209,7 +219,11 @@ function basicSetup(extra) {
|
|
|
209
219
|
)
|
|
210
220
|
|
|
211
221
|
Content(`
|
|
222
|
+
${Object.values(model.main.kit.entity || {}).some((e: any) => Object.values(e.op || {}).some((o: any) => (o.points || []).some((p: any) => p.contract && JSON.parse(p.contract.json).live))) ? `if (process.env.${PROJENVNAME}_TEST_LIVE === 'TRUE') { t.skip('Covered by live operation scenarios'); return }` : ''}
|
|
212
223
|
const setup = basicSetup()
|
|
224
|
+
if (setup.live) {
|
|
225
|
+
return runLiveEntity(setup, ${JSON.stringify(entity)}, ${JSON.stringify(basicflow)}, '${nom(entity, 'Name')}')
|
|
226
|
+
}
|
|
213
227
|
const client = setup.client
|
|
214
228
|
const struct = setup.struct
|
|
215
229
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cmp, File, Content, entityCollection, envName, serverVariables, serverVarEnv } from '@voxgig/sdkgen'
|
|
2
|
+
import { nom } from '@voxgig/apidef'
|
|
3
|
+
|
|
4
|
+
const TestLive = cmp(function TestLive(props: any) {
|
|
5
|
+
const model = props.ctx$.model
|
|
6
|
+
const plan: any[] = []
|
|
7
|
+
for (const entity of Object.values(entityCollection(model)) as any[]) {
|
|
8
|
+
if (entity.active === false) continue
|
|
9
|
+
for (const [op, operation] of Object.entries(entity.op || {}) as any[]) {
|
|
10
|
+
for (const point of operation.points || []) {
|
|
11
|
+
const facts = point.contract ? JSON.parse(point.contract.json) : {}
|
|
12
|
+
if (model.main.kit.info?.auth === false) facts.security = []
|
|
13
|
+
const same = operation.points.filter((p: any) => JSON.stringify(p.select || {}) === JSON.stringify(point.select || {}))
|
|
14
|
+
plan.push({ entity: entity.name, accessor: nom(entity, 'Name'), op,
|
|
15
|
+
id: point.contract?.id || point.method + ' ' + point.orig, contractVersion: point.contract?.version, kind: point.kind, graphql: point.graphql, path: point.orig, method: point.method,
|
|
16
|
+
action: point.select?.$action, rename: point.rename, args: point.args, facts, reachable: same.length === 1 })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!plan.some(p => p.facts.live)) return
|
|
21
|
+
const server = serverVariables(model).map((v: any) => JSON.stringify(v.name) + ': process.env[' + JSON.stringify(serverVarEnv(envName(model), v.name)) + '] ?? ' + JSON.stringify(v.dflt)).join(', ')
|
|
22
|
+
File({ name: 'live.test.js' }, () => Content(`const { test } = require('node:test')
|
|
23
|
+
const { SDK } = require('..')
|
|
24
|
+
const { runLiveScenarios } = require('./live-scenarios')
|
|
25
|
+
const { loadEnvLocal } = require('./utility')
|
|
26
|
+
loadEnvLocal(__dirname + '/../.env.local')
|
|
27
|
+
test('live operation coverage', { skip: process.env.${envName(model)}_TEST_LIVE !== 'TRUE' }, async () => {
|
|
28
|
+
await runLiveScenarios(SDK, ${JSON.stringify(plan, null, 2)}, '${envName(model)}', { server: { ${server} }, secret: process.env.${envName(model)}_SECRET })
|
|
29
|
+
})
|
|
30
|
+
`))
|
|
31
|
+
})
|
|
32
|
+
export { TestLive }
|
|
@@ -7,6 +7,7 @@ import { cmp, each, Folder, entityCollection,
|
|
|
7
7
|
TestControl } from '@voxgig/sdkgen'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
import { TestLive } from './TestLive_js'
|
|
10
11
|
import { TestDirect } from './TestDirect_js'
|
|
11
12
|
import { TestEntity } from './TestEntity_js'
|
|
12
13
|
|
|
@@ -19,6 +20,7 @@ const Test = cmp(function Test(props: any) {
|
|
|
19
20
|
|
|
20
21
|
// Write-once: a project's edited control file survives regeneration.
|
|
21
22
|
TestControl({ target, dir: 'test' })
|
|
23
|
+
TestLive({ target })
|
|
22
24
|
|
|
23
25
|
Folder({ name: 'entity' }, () => {
|
|
24
26
|
const entity = each(entityCollection(model))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
const envlocal = __dirname + '/../../../.env.local'
|
|
3
|
-
require('
|
|
3
|
+
require('../../utility').loadEnvLocal(envlocal)
|
|
4
4
|
|
|
5
5
|
const { test, describe, afterEach } = require('node:test')
|
|
6
6
|
const assert = require('node:assert')
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
const envlocal = __dirname + '/../../../.env.local'
|
|
3
|
-
require('
|
|
3
|
+
require('../../utility').loadEnvLocal(envlocal)
|
|
4
4
|
|
|
5
5
|
const Path = require('node:path')
|
|
6
6
|
const Fs = require('node:fs')
|
|
7
7
|
|
|
8
8
|
const { test, describe, afterEach } = require('node:test')
|
|
9
9
|
const assert = require('node:assert')
|
|
10
|
+
const { createLiveTransport } = require('../../live-runner')
|
|
11
|
+
const { runLiveEntity } = require('../../live-entity')
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
const { ProjectNameSDK, BaseFeature, stdutil, config } = require('../../..')
|
|
@@ -36,7 +38,7 @@ describe('EntityNameEntity', async () => {
|
|
|
36
38
|
})
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
test('basic', async () => {
|
|
41
|
+
test('basic', async (t) => {
|
|
40
42
|
// <[SLOT:basic]>
|
|
41
43
|
})
|
|
42
44
|
})
|
|
@@ -119,7 +119,7 @@ ${entityLines}
|
|
|
119
119
|
// That only holds if the run pass can SEE a parse failure — and without
|
|
120
120
|
// these two patterns it could not, so a syntax error in a runnable example
|
|
121
121
|
// would have passed both gates.
|
|
122
|
-
private const FATAL = '/(Call to undefined method|Call to undefined function|Call to a member function|ArgumentCountError|Too few arguments|Undefined constant|Uncaught TypeError|ParseError|Parse error)/';
|
|
122
|
+
private const FATAL = '/(Call to undefined method|Call to undefined function|Call to a member function|ArgumentCountError|Too few arguments|Undefined constant|Uncaught TypeError|ParseError|Parse error|Allowed memory size|Cannot use object of type)/';
|
|
123
123
|
|
|
124
124
|
// The three documentation sources this gate covers.
|
|
125
125
|
private function docs(): array
|
|
@@ -374,20 +374,14 @@ ${entityLines}
|
|
|
374
374
|
|
|
375
375
|
$driver = $dir . '/_driver.php';
|
|
376
376
|
file_put_contents($driver, $this->batchDriver($paths));
|
|
377
|
-
$
|
|
378
|
-
$rc = 0;
|
|
379
|
-
exec('php ' . escapeshellarg($driver) . ' 2>&1', $out, $rc);
|
|
380
|
-
$text = implode("\\n", $out);
|
|
377
|
+
[$text, $rc] = $this->runOutput('php ' . escapeshellarg($driver));
|
|
381
378
|
|
|
382
379
|
foreach ($runnable as $i => $blk) {
|
|
383
380
|
$seg = $this->batchSegment($text, $i);
|
|
384
381
|
if ($seg === null) {
|
|
385
382
|
// No END marker: the batch died inside or before this
|
|
386
383
|
// snippet. Re-run it alone so the verdict is isolated.
|
|
387
|
-
$
|
|
388
|
-
$src = 0;
|
|
389
|
-
exec('php ' . escapeshellarg($paths[$i]) . ' 2>&1', $solo, $src);
|
|
390
|
-
$seg = implode("\\n", $solo);
|
|
384
|
+
[$seg, $src] = $this->runOutput('php ' . escapeshellarg($paths[$i]));
|
|
391
385
|
$rc = $src;
|
|
392
386
|
}
|
|
393
387
|
if (preg_match(self::FATAL, $seg) === 1) {
|
|
@@ -410,6 +404,33 @@ ${entityLines}
|
|
|
410
404
|
* in flight, so the harness can tell "this one died" from "the batch
|
|
411
405
|
* stopped before reaching it", and re-run only what it must.
|
|
412
406
|
*/
|
|
407
|
+
private function runOutput(string $command): array
|
|
408
|
+
{
|
|
409
|
+
// A printed entity can include the client graph. Scan the stream;
|
|
410
|
+
// retaining all output (and a second joined copy) exhausts PHP's heap.
|
|
411
|
+
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['redirect', 1]], $pipes);
|
|
412
|
+
if (!is_resource($process)) throw new \RuntimeException('Could not run README example');
|
|
413
|
+
$summary = '';
|
|
414
|
+
$tail = '';
|
|
415
|
+
$reported = false;
|
|
416
|
+
while (($chunk = fgets($pipes[1], 8192)) !== false) {
|
|
417
|
+
if (preg_match('/@@VOX(BEGIN|END) [0-9]+/', $chunk, $marker)) {
|
|
418
|
+
$summary .= "\\n" . $marker[0] . "\\n";
|
|
419
|
+
if ($marker[1] === 'BEGIN') $reported = false;
|
|
420
|
+
$tail = '';
|
|
421
|
+
}
|
|
422
|
+
$window = $tail . $chunk;
|
|
423
|
+
if (!$reported && preg_match(self::FATAL, $window, $error, PREG_OFFSET_CAPTURE)) {
|
|
424
|
+
$summary .= "\\n" . substr($window, max(0, $error[0][1] - 100), 512) . "\\n";
|
|
425
|
+
$reported = true;
|
|
426
|
+
}
|
|
427
|
+
// Keep enough overlap to catch an error phrase split by fgets.
|
|
428
|
+
$tail = substr($window, -256);
|
|
429
|
+
}
|
|
430
|
+
fclose($pipes[1]);
|
|
431
|
+
return [$summary, proc_close($process)];
|
|
432
|
+
}
|
|
433
|
+
|
|
413
434
|
private function batchDriver(array $paths): string
|
|
414
435
|
{
|
|
415
436
|
// NOWDOC, not heredoc. The driver is PHP source that must survive
|
|
@@ -21,7 +21,12 @@ const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
|
|
|
21
21
|
// Pick an entity with a real op (prefer a read op) — never fabricate a
|
|
22
22
|
// `load` on an op-less entity like Cloudsmith's `Abort`. primaryOp is null
|
|
23
23
|
// only when NO entity exposes any op (a direct()-only SDK).
|
|
24
|
-
|
|
24
|
+
// Prefer records that the offline store can seed and look up by ID.
|
|
25
|
+
// An ID-less singleton has no key for the mock transport to resolve.
|
|
26
|
+
const seedable = Object.fromEntries(Object.entries(entity).filter(
|
|
27
|
+
([, value]: any) => entityIdField(value) && Object.keys(value.op || {}).length))
|
|
28
|
+
const { entity: exampleEntity, primaryOp } = pickExampleEntity(
|
|
29
|
+
Object.keys(seedable).length ? seedable : entity)
|
|
25
30
|
const eName = exampleEntity ? nom(exampleEntity, 'Name') : 'Entity'
|
|
26
31
|
// Model-driven id key: null when the entity has no id-like field.
|
|
27
32
|
const idF = exampleEntity ? entityIdField(exampleEntity) : null
|
|
@@ -51,10 +56,10 @@ const ReadmeHowto = cmp(function ReadmeHowto(props: any) {
|
|
|
51
56
|
// The op-driven test-mode line, shown only when the SDK has an entity op.
|
|
52
57
|
// A direct()-only SDK (no ops anywhere) shows a direct() call instead.
|
|
53
58
|
const testModeExample = primaryOp
|
|
54
|
-
? `// Entity ops return the ENTITY (throws on error);
|
|
59
|
+
? `// ${primaryOp === 'list' ? 'list() returns entity instances' : 'Entity ops return the ENTITY'} (throws on error);
|
|
55
60
|
// call data_get() for the mock record.
|
|
56
61
|
$${eName.toLowerCase()} = $client->${phpEntityAccessor(eName)}()->${primaryOp}(${testCallArg});
|
|
57
|
-
print_r($${eName.toLowerCase()});`
|
|
62
|
+
print_r(${primaryOp === 'list' ? `array_map(fn($item) => $item->data_get(), $${eName.toLowerCase()})` : `$${eName.toLowerCase()}->data_get()`});`
|
|
58
63
|
: `$result = $client->direct(["path" => "/api/resource", "method" => "GET"]);
|
|
59
64
|
print_r($result);`
|
|
60
65
|
|
|
@@ -81,9 +81,10 @@ $client = ${ctor};
|
|
|
81
81
|
|
|
82
82
|
\`\`\`php
|
|
83
83
|
try {
|
|
84
|
-
// list() returns
|
|
84
|
+
// list() returns entity instances; data_get() reads each record.
|
|
85
85
|
$${eName.toLowerCase()}s = $client->${phpEntityAccessor(eName)}()->list();
|
|
86
|
-
foreach ($${eName.toLowerCase()}s as $
|
|
86
|
+
foreach ($${eName.toLowerCase()}s as $record) {
|
|
87
|
+
$item = $record->data_get();
|
|
87
88
|
echo ${itemPrint} . "\\n";
|
|
88
89
|
}
|
|
89
90
|
} catch (\\Throwable $err) {
|
|
@@ -122,7 +123,7 @@ ${neName} is nested under ${parentName}, so provide the \`${parentParam}\`.
|
|
|
122
123
|
try {
|
|
123
124
|
// load() returns the ENTITY — call data_get() for the ${neName} record (throws on error).
|
|
124
125
|
$${neVar} = $client->${phpEntityAccessor(neName)}()->load([${neMatch.join(', ')}]);
|
|
125
|
-
print_r($${neVar});
|
|
126
|
+
print_r($${neVar}->data_get());
|
|
126
127
|
} catch (\\Throwable $err) {
|
|
127
128
|
echo "Error: " . $err->getMessage();
|
|
128
129
|
}
|
|
@@ -150,7 +151,7 @@ try {
|
|
|
150
151
|
try {
|
|
151
152
|
// load() returns the ENTITY — call data_get() for the ${eName} record (throws on error).
|
|
152
153
|
$${eName.toLowerCase()} = $client->${phpEntityAccessor(eName)}()->load(${loadArg});
|
|
153
|
-
print_r($${eName.toLowerCase()});
|
|
154
|
+
print_r($${eName.toLowerCase()}->data_get());
|
|
154
155
|
} catch (\\Throwable $err) {
|
|
155
156
|
echo "Error: " . $err->getMessage();
|
|
156
157
|
}
|
|
@@ -52,7 +52,7 @@ $client = ${ctor};
|
|
|
52
52
|
if (opnames.includes('list')) {
|
|
53
53
|
Content(`// List all ${eName.toLowerCase()}s (returns an array; throws on error)
|
|
54
54
|
$${eName.toLowerCase()}s = $client->${phpEntityAccessor(eName)}()->list();
|
|
55
|
-
print_r($${eName.toLowerCase()}s);
|
|
55
|
+
print_r(array_map(fn($item) => $item->data_get(), $${eName.toLowerCase()}s));
|
|
56
56
|
`)
|
|
57
57
|
hasCall = true
|
|
58
58
|
}
|
|
@@ -73,7 +73,7 @@ print_r($${eName.toLowerCase()}s);
|
|
|
73
73
|
Content(`
|
|
74
74
|
// Load a specific ${eName.toLowerCase()} (returns the ENTITY; call data_get() for the record; throws on error)
|
|
75
75
|
$${eName.toLowerCase()} = $client->${phpEntityAccessor(eName)}()->load(${loadArg});
|
|
76
|
-
print_r($${eName.toLowerCase()});
|
|
76
|
+
print_r($${eName.toLowerCase()}->data_get());
|
|
77
77
|
`)
|
|
78
78
|
hasCall = true
|
|
79
79
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
PUBLISHER_URL,
|
|
14
14
|
packageVersion,
|
|
15
15
|
authorInfo,
|
|
16
|
-
targetFeatures,
|
|
16
|
+
targetFeatures, envName,
|
|
17
17
|
} from '@voxgig/sdkgen'
|
|
18
18
|
|
|
19
19
|
|
|
@@ -83,6 +83,10 @@ const Package = cmp(async function Package(props: any) {
|
|
|
83
83
|
// are always included by npm and need no entry.
|
|
84
84
|
files: ['dist', 'src'],
|
|
85
85
|
scripts: {
|
|
86
|
+
...(Object.values(model.main.kit.entity || {}).some((e: any) => Object.values(e.op || {}).some((o: any) => (o.points || []).some((p: any) => p.contract && JSON.parse(p.contract.json).live))) ? {
|
|
87
|
+
'test:live': `npm run build && ${envName(model)}_TEST_LIVE=TRUE node --test dist-test/live.test.js`,
|
|
88
|
+
} : {}),
|
|
89
|
+
|
|
86
90
|
// `test` and `test-coverage` run the COMPILED suite in dist-test/, which
|
|
87
91
|
// a fresh clone does not have — the glob then matches nothing and the
|
|
88
92
|
// run reports "tests 0, pass 0, fail 0" and exits 0. A green suite that
|
|
@@ -190,6 +190,11 @@ Then run:
|
|
|
190
190
|
cd ts && npm test
|
|
191
191
|
\`\`\`
|
|
192
192
|
|
|
193
|
+
Live entity tests continue independent operations after errors and attempt
|
|
194
|
+
supported cleanup. Their final result reports failures and missing prerequisites
|
|
195
|
+
after the remaining work completes. The model and test inputs determine which
|
|
196
|
+
API operations the generated scenarios cover.
|
|
197
|
+
|
|
193
198
|
`)
|
|
194
199
|
|
|
195
200
|
})
|
|
@@ -104,6 +104,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
104
104
|
|
|
105
105
|
Slot({ name: 'directSetup' }, () => {
|
|
106
106
|
Content(`
|
|
107
|
+
function liveScenariosActive() { return ${Object.values(model.main.kit.entity || {}).some((e: any) => Object.values(e.op || {}).some((o: any) => (o.points || []).some((p: any) => p.contract && JSON.parse(p.contract.json).live)))} && process.env.${PROJECTNAME}_TEST_LIVE === 'TRUE' }
|
|
107
108
|
function directSetup(mockres?: any) {
|
|
108
109
|
const calls: any[] = []
|
|
109
110
|
|
|
@@ -115,10 +116,11 @@ function directSetup(mockres?: any) {
|
|
|
115
116
|
const live = 'TRUE' === env.${PROJECTNAME}_TEST_LIVE
|
|
116
117
|
|
|
117
118
|
if (live) {
|
|
119
|
+
const transport = createLiveTransport()
|
|
118
120
|
// Merged so the generated fields win: sdk-test-control.json's
|
|
119
121
|
// test.client.options adds to the live client, it does not redirect it.
|
|
120
122
|
const client = new ${nom(model.const, 'Name')}SDK(
|
|
121
|
-
Object.assign({}, liveClientOptions(), {
|
|
123
|
+
Object.assign({}, liveClientOptions(), { system: { fetch: transport.fetch },${apikeyLiveField}${serverLiveField}
|
|
122
124
|
}))
|
|
123
125
|
|
|
124
126
|
let idmap: any = env['${entidEnvVar}']
|
|
@@ -126,7 +128,7 @@ function directSetup(mockres?: any) {
|
|
|
126
128
|
idmap = JSON.parse(idmap)
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
return { client, calls, live, idmap }
|
|
131
|
+
return { client, calls, live, idmap, transport }
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
const mockFetch = async (url: string, init: any) => {
|
|
@@ -246,7 +248,7 @@ ${varAsserts}`
|
|
|
246
248
|
// "compare the live server against the mock's script" - a suite that
|
|
247
249
|
// could not pass against any real API, including this project's own.
|
|
248
250
|
assert(result.ok === true,
|
|
249
|
-
'
|
|
251
|
+
'Live request failed: HTTP ' + result.status)
|
|
250
252
|
assert(result.status >= 200 && result.status < 300)
|
|
251
253
|
assert(null != result.data)
|
|
252
254
|
} else {
|
|
@@ -263,6 +265,7 @@ ${offlineChecks} }`
|
|
|
263
265
|
|
|
264
266
|
Content(`
|
|
265
267
|
test('direct-${opname}-${entity.name}', async (t: any) => {
|
|
268
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
266
269
|
const setup = directSetup()
|
|
267
270
|
if (maybeSkipControl(t, 'direct', 'direct-${opname}-${entity.name}', setup.live)) return
|
|
268
271
|
${skipMissingLine} const { client, calls } = setup
|
|
@@ -418,16 +421,15 @@ ${liveQueryPrefix} const listResult: any = await client.direct({
|
|
|
418
421
|
${listParamLines}
|
|
419
422
|
},
|
|
420
423
|
})
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
+
assert(listResult.ok && listResult.status >= 200 && listResult.status < 300,
|
|
425
|
+
'Live list discovery failed')
|
|
424
426
|
const listArr = unwrapListData(listResult.data)
|
|
425
427
|
if (null == listArr || listArr.length === 0) {
|
|
426
|
-
|
|
428
|
+
throw new Error('Live load blocked: discovery returned no entities')
|
|
427
429
|
}
|
|
428
430
|
const candidateId = ${jsOptProp('listArr[0]', idParamName)} ?? listArr[0]?.id
|
|
429
431
|
if (null == candidateId) {
|
|
430
|
-
|
|
432
|
+
throw new Error('Live load blocked: discovery returned no usable identity')
|
|
431
433
|
}
|
|
432
434
|
${jsProp('params', idParamName)} = candidateId
|
|
433
435
|
${ancestorParamLines}
|
|
@@ -479,7 +481,7 @@ ${paramAsserts}`
|
|
|
479
481
|
// "compare the live server against the mock's script" - a suite that
|
|
480
482
|
// could not pass against any real API, including this project's own.
|
|
481
483
|
assert(result.ok === true,
|
|
482
|
-
'
|
|
484
|
+
'Live request failed: HTTP ' + result.status)
|
|
483
485
|
assert(result.status >= 200 && result.status < 300)
|
|
484
486
|
assert(null != result.data)
|
|
485
487
|
} else {
|
|
@@ -496,6 +498,7 @@ ${offlineChecks} }`
|
|
|
496
498
|
|
|
497
499
|
Content(`
|
|
498
500
|
test('direct-load-${entity.name}', async (t: any) => {
|
|
501
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
499
502
|
const setup = directSetup({ id: 'direct01' })
|
|
500
503
|
if (maybeSkipControl(t, 'direct', 'direct-load-${entity.name}', setup.live)) return
|
|
501
504
|
${skipMissingLine} const { client, calls } = setup
|
|
@@ -607,9 +610,9 @@ ${paramAsserts}`
|
|
|
607
610
|
// "compare the live server against the mock's script" - a suite that
|
|
608
611
|
// could not pass against any real API, including this project's own.
|
|
609
612
|
assert(result.ok === true,
|
|
610
|
-
'
|
|
613
|
+
'Live request failed: HTTP ' + result.status)
|
|
611
614
|
assert(result.status >= 200 && result.status < 300)
|
|
612
|
-
assert(
|
|
615
|
+
assert(Array.isArray(unwrapListData(result.data)), 'Expected live list response')
|
|
613
616
|
} else {
|
|
614
617
|
${offlineChecks} }` :
|
|
615
618
|
` if (setup.live) {
|
|
@@ -628,6 +631,7 @@ ${offlineChecks} }`
|
|
|
628
631
|
|
|
629
632
|
Content(`
|
|
630
633
|
test('direct-list-${entity.name}', async (t: any) => {
|
|
634
|
+
if (liveScenariosActive()) { t.skip('Covered by live operation scenarios'); return }
|
|
631
635
|
const setup = directSetup([{ id: 'direct01' }, { id: 'direct02' }])
|
|
632
636
|
if (maybeSkipControl(t, 'direct', 'direct-list-${entity.name}', setup.live)) return
|
|
633
637
|
${skipMissingLine} const { client, calls } = setup
|