@voxgig/sdkgen 4.16.0 → 4.17.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/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 +1 -1
- package/dist/sdkgen.js +22 -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/c/Makefile +16 -11
- package/project/.sdk/tm/c/feature/secrets/sekreto/json.c +3 -3
- 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/php/test/feature/secrets/SecretsTest.php +22 -20
- 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 +15 -94
- 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
|
@@ -169,13 +169,6 @@ function basicSetup(extra?: any) {
|
|
|
169
169
|
}]
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
// Detect whether the user provided a real ENTID JSON via env var. The
|
|
173
|
-
// basic flow consumes synthetic IDs from the fixture file; without an
|
|
174
|
-
// override those synthetic IDs reach the live API and 4xx. Surface this
|
|
175
|
-
// to the test so it can skip rather than fail.
|
|
176
|
-
const idmapEnvVal = process.env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
177
|
-
const idmapOverridden = null != idmapEnvVal && idmapEnvVal.trim().startsWith('{')
|
|
178
|
-
|
|
179
172
|
const env = envOverride({
|
|
180
173
|
'${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID': idmap,
|
|
181
174
|
'${PROJENVNAME}_TEST_LIVE': 'FALSE',
|
|
@@ -186,7 +179,13 @@ function basicSetup(extra?: any) {
|
|
|
186
179
|
|
|
187
180
|
const live = 'TRUE' === env.${PROJENVNAME}_TEST_LIVE
|
|
188
181
|
|
|
182
|
+
const transport = createLiveTransport()
|
|
189
183
|
if (live) {
|
|
184
|
+
const rawIds = process.env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
185
|
+
idmap = rawIds && rawIds.trim() ? JSON.parse(rawIds) : {}
|
|
186
|
+
if (!idmap || Array.isArray(idmap) || typeof idmap !== 'object') {
|
|
187
|
+
throw new Error('Live ENTID must be a JSON object')
|
|
188
|
+
}
|
|
190
189
|
client = new ${model.Name}SDK(merge([
|
|
191
190
|
// FIRST, so the generated fields below win: sdk-test-control.json's
|
|
192
191
|
// test.client.options adds to the live client, it does not redirect it.
|
|
@@ -198,7 +197,8 @@ function basicSetup(extra?: any) {
|
|
|
198
197
|
// argument at all - so a bare 'extra' silently discarded the apikey
|
|
199
198
|
// and server values above and handed the SDK undefined. Harmless
|
|
200
199
|
// while there was nothing in that object; not harmless now.
|
|
201
|
-
extra || {}
|
|
200
|
+
extra || {},
|
|
201
|
+
{ system: { fetch: transport.fetch } }
|
|
202
202
|
]))
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -211,7 +211,7 @@ function basicSetup(extra?: any) {
|
|
|
211
211
|
data: entityData,
|
|
212
212
|
explain: 'TRUE' === env.${PROJENVNAME}_TEST_EXPLAIN,
|
|
213
213
|
live,
|
|
214
|
-
|
|
214
|
+
transport,
|
|
215
215
|
now: Date.now(),
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -239,16 +239,13 @@ function basicSetup(extra?: any) {
|
|
|
239
239
|
Content(`
|
|
240
240
|
const live = 'TRUE' === process.env.${PROJENVNAME}_TEST_LIVE
|
|
241
241
|
for (const op of ${flowOpsLiteral}) {
|
|
242
|
-
if (maybeSkipControl(t, 'entityOp', '${entity.name}.' + op, live)) return
|
|
242
|
+
if (!live && maybeSkipControl(t, 'entityOp', '${entity.name}.' + op, live)) return
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
${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 (live) { t.skip('Covered by live operation scenarios'); return }` : ''}
|
|
245
246
|
const setup = basicSetup()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Skip live runs unless the user provided a real ENTID env override.
|
|
249
|
-
if (setup.syntheticOnly) {
|
|
250
|
-
t.skip('live entity test uses synthetic IDs from fixture — set ${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID JSON to run live')
|
|
251
|
-
return
|
|
247
|
+
if (setup.live) {
|
|
248
|
+
return runLiveEntity(setup, ${JSON.stringify(entity)}, ${JSON.stringify(basicflow)}, '${nom(entity, 'Name')}')
|
|
252
249
|
}
|
|
253
250
|
const client = setup.client
|
|
254
251
|
const struct = setup.struct
|
|
@@ -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.ts' }, () => Content(`import { test } from 'node:test'
|
|
23
|
+
import { SDK } from '..'
|
|
24
|
+
import { runLiveScenarios } from './live-scenarios'
|
|
25
|
+
import { loadEnvLocal } from './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 }
|
|
@@ -9,6 +9,7 @@ import { cmp, each, Folder, entityCollection,
|
|
|
9
9
|
|
|
10
10
|
// import { Quick } from './Quick_ts'
|
|
11
11
|
// import { TestMain } from './TestMain_ts'
|
|
12
|
+
import { TestLive } from './TestLive_ts'
|
|
12
13
|
import { TestDirect } from './TestDirect_ts'
|
|
13
14
|
import { TestEntity } from './TestEntity_ts'
|
|
14
15
|
import { ReadmeExampleTest } from './ReadmeExampleTest_ts'
|
|
@@ -23,6 +24,7 @@ const Test = cmp(function Test(props: any) {
|
|
|
23
24
|
|
|
24
25
|
// Write-once: a project's edited control file survives regeneration.
|
|
25
26
|
TestControl({ target, dir: 'test' })
|
|
27
|
+
TestLive({ target })
|
|
26
28
|
// Quick({ target })
|
|
27
29
|
// TestMain({ target })
|
|
28
30
|
|
|
@@ -5,6 +5,8 @@ import * as Fs from 'node:fs'
|
|
|
5
5
|
|
|
6
6
|
import { test, describe, afterEach } from 'node:test'
|
|
7
7
|
import assert from 'node:assert'
|
|
8
|
+
import { createLiveTransport } from '../../live-runner'
|
|
9
|
+
import { runLiveEntity } from '../../live-entity'
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
import { ProjectNameSDK, BaseFeature, stdutil } from '../../..'
|
|
@@ -15,8 +15,10 @@ CFLAGS ?= -std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -O0 -g
|
|
|
15
15
|
# point.h / types.h shadow nothing: the SDK's own value.h (utility/struct)
|
|
16
16
|
# is reached by its neighbours through the including file's directory,
|
|
17
17
|
# which a quoted include searches before any -I.
|
|
18
|
-
PAYLOAD_DIRS := $(
|
|
19
|
-
|
|
18
|
+
PAYLOAD_DIRS := $(sort $(dir $(wildcard feature/*/*/*.h)))
|
|
19
|
+
# Payload headers are quoted includes; they must not shadow system headers
|
|
20
|
+
# such as <regex.h> with the plugin library's own regex.h.
|
|
21
|
+
INC := -I core -I utility/struct -I feature $(addprefix -iquote ,$(PAYLOAD_DIRS))
|
|
20
22
|
# The vendored omni test runner (tests/vendor/omni) is TEST-ONLY: it is
|
|
21
23
|
# reachable from tests/ alone, never from LIB_SRCS, so it cannot enter the
|
|
22
24
|
# shipped libsdk.a. That is why it lives under tests/ and not utility/.
|
|
@@ -74,6 +76,18 @@ all: build
|
|
|
74
76
|
|
|
75
77
|
build: libsdk.a
|
|
76
78
|
|
|
79
|
+
# Keep this before the generic rule for GNU make 3.81 on macOS.
|
|
80
|
+
# omni's util.c includes <regex.h>. CFLAGS is `?=`, so an operator override
|
|
81
|
+
# can drop the -D_GNU_SOURCE that exposes it; -D_POSIX_C_SOURCE is the
|
|
82
|
+
# minimum omni itself compiles under (its own test/run.c sets exactly this).
|
|
83
|
+
# _DARWIN_C_SOURCE is needed BESIDE it, not instead of it: on Darwin,
|
|
84
|
+
# defining _POSIX_C_SOURCE alone LOWERS __DARWIN_C_LEVEL and hides regex_t,
|
|
85
|
+
# so the same flag that exposes the header on glibc hides it on macOS. Each
|
|
86
|
+
# platform ignores the other's macro.
|
|
87
|
+
tests/vendor/omni/%.o: tests/vendor/omni/%.c
|
|
88
|
+
$(CC) $(CFLAGS) -D_POSIX_C_SOURCE=200809L -D_DARWIN_C_SOURCE -c $< -o $@
|
|
89
|
+
|
|
90
|
+
|
|
77
91
|
%.o: %.c
|
|
78
92
|
$(CC) $(CFLAGS) $(INC) -c $< -o $@
|
|
79
93
|
|
|
@@ -87,15 +101,6 @@ libsdk.a: $(LIB_OBJS)
|
|
|
87
101
|
@rm -f $@
|
|
88
102
|
ar rcs $@ $(LIB_OBJS)
|
|
89
103
|
|
|
90
|
-
# omni's util.c includes <regex.h>. CFLAGS is `?=`, so an operator override
|
|
91
|
-
# can drop the -D_GNU_SOURCE that exposes it; -D_POSIX_C_SOURCE is the
|
|
92
|
-
# minimum omni itself compiles under (its own test/run.c sets exactly this).
|
|
93
|
-
# _DARWIN_C_SOURCE is needed BESIDE it, not instead of it: on Darwin,
|
|
94
|
-
# defining _POSIX_C_SOURCE alone LOWERS __DARWIN_C_LEVEL and hides regex_t,
|
|
95
|
-
# so the same flag that exposes the header on glibc hides it on macOS. Each
|
|
96
|
-
# platform ignores the other's macro.
|
|
97
|
-
tests/vendor/omni/%.o: tests/vendor/omni/%.c
|
|
98
|
-
$(CC) $(CFLAGS) -D_POSIX_C_SOURCE=200809L -D_DARWIN_C_SOURCE -c $< -o $@
|
|
99
104
|
|
|
100
105
|
libomni.a: $(OMNI_OBJS)
|
|
101
106
|
@rm -f $@
|
|
@@ -93,7 +93,7 @@ static void pushitem(sek_pool *pool, sek_json *arr, sek_json *item) {
|
|
|
93
93
|
arr->itemlen++;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
static void
|
|
96
|
+
static void sek_json_assign_member(sek_pool *pool, sek_json *obj, const char *key, sek_json *val) {
|
|
97
97
|
size_t index;
|
|
98
98
|
|
|
99
99
|
for (index = 0; index < obj->maplen; index++) {
|
|
@@ -128,7 +128,7 @@ void sek_json_push(sek_pool *pool, sek_json *arr, sek_json *item) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
void sek_json_set(sek_pool *pool, sek_json *obj, const char *key, sek_json *val) {
|
|
131
|
-
|
|
131
|
+
sek_json_assign_member(pool, obj, key, val);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/* ---- reads --------------------------------------------------------- */
|
|
@@ -607,7 +607,7 @@ static sek_json *readobject(reader *rd) {
|
|
|
607
607
|
return NULL;
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
-
|
|
610
|
+
sek_json_assign_member(rd->pool, out, key, val);
|
|
611
611
|
skipspace(rd);
|
|
612
612
|
|
|
613
613
|
if (rd->at >= rd->len) {
|
|
@@ -1394,9 +1394,16 @@ public class SecretsFeatureTest
|
|
|
1394
1394
|
Parallel.For(0, 4, i =>
|
|
1395
1395
|
client.Direct(new Dictionary<string, object?> { ["path"] = "/p" + i }));
|
|
1396
1396
|
|
|
1397
|
+
// REPORTS WHAT WENT OUT, not just how many. This assertion failed
|
|
1398
|
+
// once in CI and passed on re-run, and the count alone could not
|
|
1399
|
+
// say which of two very different faults it was: several
|
|
1400
|
+
// purchases (the coalescing missed) or none (the exchange never
|
|
1401
|
+
// ran). By the time anyone looked, the re-run had replaced the
|
|
1402
|
+
// job log and the number was gone. Report() exists for exactly
|
|
1403
|
+
// this, and the sibling assertions already use it.
|
|
1397
1404
|
Assert.True(1 == w.Token().Count,
|
|
1398
|
-
"four operations at once must
|
|
1399
|
-
w.Token().Count);
|
|
1405
|
+
"four operations at once must open exactly ONE token request, saw " +
|
|
1406
|
+
w.Token().Count + " — calls: " + w.Report());
|
|
1400
1407
|
}
|
|
1401
1408
|
finally
|
|
1402
1409
|
{
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateContract = validateContract;
|
|
4
|
+
exports.synthesizeInput = synthesizeInput;
|
|
5
|
+
exports.requestContract = requestContract;
|
|
6
|
+
const live_runner_1 = require("./live-runner");
|
|
7
|
+
// Deliberately conservative: unsupported constraints block preparation instead
|
|
8
|
+
// of labelling an invented value as live-valid. Errors never include values.
|
|
9
|
+
function validateContract(schema, value, direction = 'request', depth = 0) {
|
|
10
|
+
const bad = () => { throw new Error('Operation contract mismatch'); };
|
|
11
|
+
if (depth > 40 || schema === false)
|
|
12
|
+
return bad();
|
|
13
|
+
if (schema === true || schema == null)
|
|
14
|
+
return;
|
|
15
|
+
for (const key of ['if', 'then', 'else', 'dependentRequired', 'dependentSchemas', 'patternProperties', 'contains', 'unevaluatedProperties', 'unevaluatedItems']) {
|
|
16
|
+
if (schema[key] !== undefined)
|
|
17
|
+
throw new live_runner_1.LiveBlocked('Unsupported contract constraint: ' + key);
|
|
18
|
+
}
|
|
19
|
+
if (schema.$ref)
|
|
20
|
+
throw new live_runner_1.LiveBlocked('Unresolved contract reference');
|
|
21
|
+
if (value === null && (schema.nullable || schema.type === 'null' || schema.type?.includes?.('null')))
|
|
22
|
+
return;
|
|
23
|
+
for (const sub of schema.allOf || [])
|
|
24
|
+
validateContract(sub, value, direction, depth + 1);
|
|
25
|
+
for (const key of ['oneOf', 'anyOf'])
|
|
26
|
+
if (schema[key]) {
|
|
27
|
+
let matches = 0;
|
|
28
|
+
for (const sub of schema[key]) {
|
|
29
|
+
try {
|
|
30
|
+
validateContract(sub, value, direction, depth + 1);
|
|
31
|
+
matches++;
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
if (key === 'oneOf' ? matches !== 1 : matches === 0)
|
|
36
|
+
bad();
|
|
37
|
+
}
|
|
38
|
+
if (schema.not) {
|
|
39
|
+
let matched = true;
|
|
40
|
+
try {
|
|
41
|
+
validateContract(schema.not, value, direction, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
matched = false;
|
|
45
|
+
}
|
|
46
|
+
;
|
|
47
|
+
if (matched)
|
|
48
|
+
bad();
|
|
49
|
+
}
|
|
50
|
+
if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const))
|
|
51
|
+
bad();
|
|
52
|
+
if (schema.enum && !schema.enum.some((v) => JSON.stringify(v) === JSON.stringify(value)))
|
|
53
|
+
bad();
|
|
54
|
+
const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
55
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
56
|
+
if (types.length && !types.some((t) => t === kind || t === 'integer' && Number.isInteger(value)))
|
|
57
|
+
bad();
|
|
58
|
+
if (typeof value === 'number') {
|
|
59
|
+
if (!Number.isFinite(value))
|
|
60
|
+
bad();
|
|
61
|
+
if (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum)
|
|
62
|
+
bad();
|
|
63
|
+
if (typeof schema.exclusiveMinimum === 'number' && value <= schema.exclusiveMinimum || schema.exclusiveMinimum === true && value <= schema.minimum)
|
|
64
|
+
bad();
|
|
65
|
+
if (typeof schema.exclusiveMaximum === 'number' && value >= schema.exclusiveMaximum || schema.exclusiveMaximum === true && value >= schema.maximum)
|
|
66
|
+
bad();
|
|
67
|
+
if (schema.multipleOf && Math.abs(value / schema.multipleOf - Math.round(value / schema.multipleOf)) > 1e-8)
|
|
68
|
+
bad();
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
if (value.length < (schema.minLength || 0) || value.length > (schema.maxLength ?? Infinity))
|
|
72
|
+
bad();
|
|
73
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value))
|
|
74
|
+
bad();
|
|
75
|
+
if (schema.format === 'date-time' && !Number.isFinite(Date.parse(value)))
|
|
76
|
+
bad();
|
|
77
|
+
if (schema.format === 'date' && !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
78
|
+
bad();
|
|
79
|
+
if (schema.format === 'uuid' && !/^[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value))
|
|
80
|
+
bad();
|
|
81
|
+
if (schema.format === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
82
|
+
bad();
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
if (value.length < (schema.minItems || 0) || value.length > (schema.maxItems ?? Infinity))
|
|
86
|
+
bad();
|
|
87
|
+
if (schema.uniqueItems && new Set(value.map(v => JSON.stringify(v))).size !== value.length)
|
|
88
|
+
bad();
|
|
89
|
+
value.forEach((v, i) => validateContract(schema.prefixItems?.[i] ?? schema.items, v, direction, depth + 1));
|
|
90
|
+
}
|
|
91
|
+
else if (value !== null && typeof value === 'object') {
|
|
92
|
+
for (const key of schema.required || []) {
|
|
93
|
+
const prop = schema.properties?.[key];
|
|
94
|
+
if (direction === 'request' && prop?.readOnly || direction === 'response' && prop?.writeOnly)
|
|
95
|
+
continue;
|
|
96
|
+
if (value[key] === undefined)
|
|
97
|
+
bad();
|
|
98
|
+
}
|
|
99
|
+
for (const [key, v] of Object.entries(value)) {
|
|
100
|
+
const prop = schema.properties?.[key];
|
|
101
|
+
if (direction === 'request' && prop?.readOnly || direction === 'response' && prop?.writeOnly)
|
|
102
|
+
bad();
|
|
103
|
+
if (prop === undefined && schema.additionalProperties === false)
|
|
104
|
+
bad();
|
|
105
|
+
validateContract(prop ?? schema.additionalProperties, v, direction, depth + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function synthesizeInput(schema, explicit, depth = 0) {
|
|
110
|
+
if (depth > 30)
|
|
111
|
+
throw new live_runner_1.LiveBlocked('Request schema nesting exceeds limit');
|
|
112
|
+
if (schema?.$ref)
|
|
113
|
+
throw new live_runner_1.LiveBlocked('Unresolved request reference');
|
|
114
|
+
if (explicit !== undefined) {
|
|
115
|
+
validateContract(schema, explicit);
|
|
116
|
+
return explicit;
|
|
117
|
+
}
|
|
118
|
+
if (!schema || schema === true)
|
|
119
|
+
throw new live_runner_1.LiveBlocked('No request contract or input recipe');
|
|
120
|
+
for (const key of ['example', 'default', 'const'])
|
|
121
|
+
if (schema[key] !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
validateContract(schema, schema[key]);
|
|
124
|
+
return schema[key];
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
}
|
|
128
|
+
for (const value of schema.examples || schema.enum || []) {
|
|
129
|
+
try {
|
|
130
|
+
validateContract(schema, value);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
}
|
|
135
|
+
for (const choice of schema.oneOf || schema.anyOf || []) {
|
|
136
|
+
try {
|
|
137
|
+
const v = synthesizeInput(choice, undefined, depth + 1);
|
|
138
|
+
validateContract(schema, v);
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
}
|
|
143
|
+
let value;
|
|
144
|
+
if (schema.allOf)
|
|
145
|
+
value = Object.assign({}, ...schema.allOf.map((s) => synthesizeInput(s, undefined, depth + 1)));
|
|
146
|
+
else if (schema.type === 'object' || schema.properties) {
|
|
147
|
+
value = {};
|
|
148
|
+
for (const key of schema.required || [])
|
|
149
|
+
if (!schema.properties?.[key]?.readOnly) {
|
|
150
|
+
value[key] = synthesizeInput(schema.properties?.[key], undefined, depth + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (schema.type === 'array') {
|
|
154
|
+
const count = Math.max(1, schema.minItems || 0);
|
|
155
|
+
if (count > 16)
|
|
156
|
+
throw new live_runner_1.LiveBlocked('Required input array exceeds test payload limit');
|
|
157
|
+
value = Array.from({ length: count }, () => synthesizeInput(schema.items, undefined, depth + 1));
|
|
158
|
+
}
|
|
159
|
+
else if (schema.type === 'integer' || schema.type === 'number') {
|
|
160
|
+
value = Math.max(1, schema.minimum ?? 1);
|
|
161
|
+
if (typeof schema.exclusiveMinimum === 'number')
|
|
162
|
+
value = Math.max(value, schema.exclusiveMinimum + 1);
|
|
163
|
+
if (schema.multipleOf)
|
|
164
|
+
value = Math.ceil(value / schema.multipleOf) * schema.multipleOf;
|
|
165
|
+
}
|
|
166
|
+
else if (schema.type === 'boolean')
|
|
167
|
+
value = true;
|
|
168
|
+
else
|
|
169
|
+
throw new live_runner_1.LiveBlocked('Required input needs a guide recipe or validated example');
|
|
170
|
+
validateContract(schema, value);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function requestContract(facts) {
|
|
174
|
+
const body = facts?.requestBody;
|
|
175
|
+
if (body) {
|
|
176
|
+
const media = body.content?.['application/json'] ?? body.content?.['application/*+json'];
|
|
177
|
+
if (!media)
|
|
178
|
+
throw new live_runner_1.LiveBlocked('Unsupported request media type');
|
|
179
|
+
return { schema: media.schema, example: media.example ?? Object.values(media.examples || {}).map((v) => v.value)[0], required: body.required };
|
|
180
|
+
}
|
|
181
|
+
const arg = facts?.parameters?.find((p) => p.in === 'body');
|
|
182
|
+
return arg ? { schema: arg.schema, example: arg.example, required: arg.required } : {};
|
|
183
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runLiveEntity = runLiveEntity;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const live_contract_1 = require("./live-contract");
|
|
9
|
+
const live_runner_1 = require("./live-runner");
|
|
10
|
+
const utility_1 = require("./utility");
|
|
11
|
+
// The offline flow keeps its deterministic fixture assertions. Live flows
|
|
12
|
+
// resolve real prerequisites per operation and collect failures until done.
|
|
13
|
+
async function runLiveEntity(setup, entity, flow, accessor) {
|
|
14
|
+
const { client, transport } = setup;
|
|
15
|
+
const steps = flow.step || [];
|
|
16
|
+
const created = new Map();
|
|
17
|
+
const listed = [];
|
|
18
|
+
const marks = new Map();
|
|
19
|
+
const idField = entity.id?.field || 'id';
|
|
20
|
+
const copy = (value) => JSON.parse(JSON.stringify(value ?? {}));
|
|
21
|
+
const hasCreate = steps.some(step => step.op === 'create');
|
|
22
|
+
const report = await (0, live_runner_1.runLiveSteps)(steps.map((step, index) => {
|
|
23
|
+
const ref = step.input?.ref || entity.name + '_ref01';
|
|
24
|
+
const op = step.op;
|
|
25
|
+
const excluded = (0, utility_1.isControlSkipped)('entityOp', entity.name + '.' + op, 'live');
|
|
26
|
+
return {
|
|
27
|
+
id: entity.name + '.' + op + '.' + index,
|
|
28
|
+
cleanup: op === 'remove' || (step.valid || []).some((v) => v.apply === 'ItemNotExists'),
|
|
29
|
+
excluded: excluded.skip ? excluded.reason || 'Excluded by test control' : undefined,
|
|
30
|
+
run: async (context) => {
|
|
31
|
+
transport.enter(context);
|
|
32
|
+
const points = entity.op?.[op]?.points || [];
|
|
33
|
+
if (!points.length)
|
|
34
|
+
throw new live_runner_1.LiveBlocked('No modelled operation point');
|
|
35
|
+
const record = created.get(ref);
|
|
36
|
+
// A failed create must not turn a later update/remove into a write
|
|
37
|
+
// against an arbitrary record found by a list operation.
|
|
38
|
+
if ((op === 'update' || op === 'remove') && hasCreate && !record) {
|
|
39
|
+
throw new live_runner_1.LiveBlocked('Create did not provide a usable resource');
|
|
40
|
+
}
|
|
41
|
+
if (op === 'remove' && !record)
|
|
42
|
+
throw new live_runner_1.LiveBlocked('No resource created by this run');
|
|
43
|
+
if (op === 'remove' && !entity.id)
|
|
44
|
+
throw new live_runner_1.LiveBlocked('No modelled resource identity for cleanup');
|
|
45
|
+
if (record && ['update', 'remove'].includes(op) && entity.id &&
|
|
46
|
+
null == (record[idField] ?? record.id)) {
|
|
47
|
+
throw new live_runner_1.LiveBlocked('Created resource has no usable identity');
|
|
48
|
+
}
|
|
49
|
+
let input = op === 'create'
|
|
50
|
+
? copy(setup.data.new?.[entity.name]?.[ref]) : {};
|
|
51
|
+
for (const [name, binding] of Object.entries({ ...step.match, ...step.data })) {
|
|
52
|
+
const value = setup.idmap[binding] ?? setup.idmap[name];
|
|
53
|
+
if (undefined !== value)
|
|
54
|
+
input[name] = value;
|
|
55
|
+
}
|
|
56
|
+
const loaded = record || (op === 'load' ? listed[0] : undefined);
|
|
57
|
+
if (loaded && ['load', 'update', 'remove'].includes(op)) {
|
|
58
|
+
const id = loaded[idField] ?? loaded.id;
|
|
59
|
+
if (undefined !== id)
|
|
60
|
+
input.id = id;
|
|
61
|
+
}
|
|
62
|
+
// A nested route needing a parent id must not hide an available
|
|
63
|
+
// parameter-free route for the same operation. Use the first viable
|
|
64
|
+
// candidate; the SDK still performs its normal route selection.
|
|
65
|
+
let resolved;
|
|
66
|
+
let selected;
|
|
67
|
+
const missing = new Set();
|
|
68
|
+
for (const point of points) {
|
|
69
|
+
if (point.select?.$action !== input.$action)
|
|
70
|
+
continue;
|
|
71
|
+
const candidate = { ...input };
|
|
72
|
+
let viable = true;
|
|
73
|
+
const params = point.args?.params || [];
|
|
74
|
+
const query = (point.args?.query || []).filter((arg) => arg.reqd);
|
|
75
|
+
for (const arg of [...params, ...query]) {
|
|
76
|
+
if (undefined !== candidate[arg.name] && null !== candidate[arg.name])
|
|
77
|
+
continue;
|
|
78
|
+
const key = arg.name === 'id' ? entity.name + '01' : arg.name.replace(/_id$/, '') + '01';
|
|
79
|
+
const value = setup.idmap[key] ?? setup.idmap[arg.name] ?? loaded?.[arg.name] ?? arg.example;
|
|
80
|
+
if (undefined !== value && null !== value)
|
|
81
|
+
candidate[arg.name] = value;
|
|
82
|
+
else if (arg.reqd !== false) {
|
|
83
|
+
viable = false;
|
|
84
|
+
missing.add(arg.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (viable) {
|
|
88
|
+
resolved = candidate;
|
|
89
|
+
selected = point;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!resolved)
|
|
94
|
+
throw new live_runner_1.LiveBlocked('No usable route; missing arguments: ' + [...missing].join(', '));
|
|
95
|
+
input = resolved;
|
|
96
|
+
if (op === 'create' && selected.contract) {
|
|
97
|
+
const facts = JSON.parse(selected.contract.json);
|
|
98
|
+
const request = (0, live_contract_1.requestContract)(facts);
|
|
99
|
+
if (request.schema)
|
|
100
|
+
input = { ...(0, live_contract_1.synthesizeInput)(request.schema, facts.live?.input ?? request.example) };
|
|
101
|
+
else if (facts.protocol === 'http')
|
|
102
|
+
input = {};
|
|
103
|
+
for (const arg of selected.args?.params || [])
|
|
104
|
+
if (resolved[arg.name] !== undefined)
|
|
105
|
+
input[arg.name] = resolved[arg.name];
|
|
106
|
+
}
|
|
107
|
+
let intendedMark;
|
|
108
|
+
if (op === 'update') {
|
|
109
|
+
for (const spec of step.spec || []) {
|
|
110
|
+
if (spec.apply === 'TextFieldMark' && step.input?.textfield) {
|
|
111
|
+
const mark = { name: step.input.textfield, value: spec.def.mark + '_' + setup.now };
|
|
112
|
+
input[mark.name] = mark.value;
|
|
113
|
+
intendedMark = mark;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// A fresh entity prevents state left by a failed operation from
|
|
118
|
+
// changing the request for the next independent operation.
|
|
119
|
+
const result = await client[accessor]()[op](input);
|
|
120
|
+
if (intendedMark)
|
|
121
|
+
marks.set(ref, intendedMark);
|
|
122
|
+
if (op === 'list') {
|
|
123
|
+
(0, strict_1.default)(Array.isArray(result), 'Expected a list of entity instances');
|
|
124
|
+
const data = result.map((item) => {
|
|
125
|
+
strict_1.default.equal(typeof item?.data, 'function');
|
|
126
|
+
return item.data();
|
|
127
|
+
});
|
|
128
|
+
listed.splice(0, listed.length, ...data);
|
|
129
|
+
context.publish(data);
|
|
130
|
+
for (const validation of step.valid || []) {
|
|
131
|
+
const previous = created.get(validation.def?.ref);
|
|
132
|
+
const id = previous?.[idField] ?? previous?.id;
|
|
133
|
+
if (undefined === id)
|
|
134
|
+
continue;
|
|
135
|
+
const found = data.some((item) => (item?.[idField] ?? item?.id) === id);
|
|
136
|
+
if (validation.apply === 'ItemExists')
|
|
137
|
+
(0, strict_1.default)(found, 'Created resource missing from list');
|
|
138
|
+
if (validation.apply === 'ItemNotExists')
|
|
139
|
+
(0, strict_1.default)(!found, 'Removed resource still in list');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
strict_1.default.equal(typeof result?.data, 'function', 'Expected an entity instance');
|
|
144
|
+
const data = result.data();
|
|
145
|
+
(0, strict_1.default)(null != data, 'Expected operation data');
|
|
146
|
+
// Publish before assertion checks: cleanup still needs the id
|
|
147
|
+
// when the server created a resource with incorrect field values.
|
|
148
|
+
if (op === 'create')
|
|
149
|
+
created.set(ref, data);
|
|
150
|
+
context.publish(data);
|
|
151
|
+
if (op === 'create' && entity.id)
|
|
152
|
+
(0, strict_1.default)(null != (data[idField] ?? data.id), 'Missing created identity');
|
|
153
|
+
if (['load', 'update'].includes(op) && input.id != null && entity.id) {
|
|
154
|
+
strict_1.default.equal(data[idField] ?? data.id, input.id, 'Response identity mismatch');
|
|
155
|
+
}
|
|
156
|
+
const mark = marks.get(ref);
|
|
157
|
+
if (mark && ['update', 'load'].includes(op)) {
|
|
158
|
+
strict_1.default.equal(data[mark.name], mark.value, 'Updated field mismatch');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}), {
|
|
164
|
+
delayMs: (0, utility_1.liveDelayMs)(),
|
|
165
|
+
report: result => console.log('LIVE STEP ' + JSON.stringify(result)),
|
|
166
|
+
});
|
|
167
|
+
console.log('LIVE SUMMARY ' + JSON.stringify({ entity: entity.name, ...report }));
|
|
168
|
+
(0, live_runner_1.assertLiveReport)(report);
|
|
169
|
+
}
|