@voxgig/sdkgen 3.7.2 → 4.0.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/action/action.js +12 -12
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.js +2 -2
- package/dist/action/check.js.map +1 -1
- package/dist/action/doctor.js +4 -4
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/feature.js +1 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/kind.js +5 -5
- package/dist/action/kind.js.map +1 -1
- package/dist/action/package.js +1 -1
- package/dist/action/package.js.map +1 -1
- package/dist/action/resolve.js +2 -2
- package/dist/action/resolve.js.map +1 -1
- package/dist/action/target.js +2 -2
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/AgentGuide.js +2 -2
- package/dist/cmp/AgentGuideContent.js +9 -9
- package/dist/cmp/AgentGuideFeature.js +3 -3
- package/dist/cmp/AgentGuideFeature.js.map +1 -1
- package/dist/cmp/AgentGuideTop.js +1 -1
- package/dist/cmp/Deploy.js +3 -3
- package/dist/cmp/Deploy.js.map +1 -1
- package/dist/helpers/definition.js +7 -7
- package/dist/helpers/definition.js.map +1 -1
- package/dist/helpers/featureSource.js +2 -2
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/manifest.js +3 -3
- package/dist/helpers/manifest.js.map +1 -1
- package/dist/helpers/modelcheck.js +1 -1
- package/dist/helpers/modelcheck.js.map +1 -1
- package/dist/helpers/opShape.d.ts +2 -1
- package/dist/helpers/opShape.js +1 -0
- package/dist/helpers/opShape.js.map +1 -1
- package/dist/helpers/packageMeta.js +1 -1
- package/dist/helpers/packageMeta.js.map +1 -1
- package/dist/helpers/shipped.js +2 -2
- package/dist/helpers/shipped.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +5 -4
- package/dist/sdkgen.js.map +1 -1
- package/dist/testkit.js +7 -7
- package/dist/testkit.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.js +1 -1
- package/dist/utility.js.map +1 -1
- package/model/{sdkgen.aontu → sdkgen.aon} +14 -0
- package/package.json +3 -3
- package/project/.sdk/model/feature/feature-index.aon +24 -0
- package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +54 -21
- package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +24 -11
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +96 -0
- package/project/.sdk/tm/csharp/Makefile +1 -1
- package/project/.sdk/tm/rb/utility/struct/voxgig_struct.rb +31 -11
- package/project/.sdk/tm/swift/Makefile +1 -1
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +47 -1
- package/project/sdkgen-package.json +1 -1
- package/src/action/action.ts +12 -12
- package/src/action/check.ts +2 -2
- package/src/action/doctor.ts +5 -5
- package/src/action/feature.ts +1 -1
- package/src/action/kind.ts +5 -5
- package/src/action/package.ts +1 -1
- package/src/action/resolve.ts +2 -2
- package/src/action/target.ts +2 -2
- package/src/cmp/AgentGuide.ts +2 -2
- package/src/cmp/AgentGuideContent.ts +9 -9
- package/src/cmp/AgentGuideFeature.ts +3 -3
- package/src/cmp/AgentGuideTop.ts +1 -1
- package/src/cmp/Deploy.ts +3 -3
- package/src/helpers/definition.ts +7 -7
- package/src/helpers/featureSource.ts +2 -2
- package/src/helpers/manifest.ts +3 -3
- package/src/helpers/modelcheck.ts +1 -1
- package/src/helpers/opShape.ts +1 -0
- package/src/helpers/packageMeta.ts +1 -1
- package/src/helpers/shipped.ts +2 -2
- package/src/sdkgen.ts +4 -3
- package/src/testkit.ts +9 -9
- package/src/utility.ts +1 -1
- package/testkit.js +1 -1
- package/project/.sdk/model/feature/feature-index.aontu +0 -24
- /package/project/.sdk/model/feature/{audit.aontu → audit.aon} +0 -0
- /package/project/.sdk/model/feature/{cache.aontu → cache.aon} +0 -0
- /package/project/.sdk/model/feature/{clienttrack.aontu → clienttrack.aon} +0 -0
- /package/project/.sdk/model/feature/{debug.aontu → debug.aon} +0 -0
- /package/project/.sdk/model/feature/{idempotency.aontu → idempotency.aon} +0 -0
- /package/project/.sdk/model/feature/{log.aontu → log.aon} +0 -0
- /package/project/.sdk/model/feature/{metrics.aontu → metrics.aon} +0 -0
- /package/project/.sdk/model/feature/{netsim.aontu → netsim.aon} +0 -0
- /package/project/.sdk/model/feature/{paging.aontu → paging.aon} +0 -0
- /package/project/.sdk/model/feature/{proxy.aontu → proxy.aon} +0 -0
- /package/project/.sdk/model/feature/{ratelimit.aontu → ratelimit.aon} +0 -0
- /package/project/.sdk/model/feature/{rbac.aontu → rbac.aon} +0 -0
- /package/project/.sdk/model/feature/{retry.aontu → retry.aon} +0 -0
- /package/project/.sdk/model/feature/{streaming.aontu → streaming.aon} +0 -0
- /package/project/.sdk/model/feature/{telemetry.aontu → telemetry.aon} +0 -0
- /package/project/.sdk/model/feature/{test.aontu → test.aon} +0 -0
- /package/project/.sdk/model/feature/{timeout.aontu → timeout.aon} +0 -0
- /package/project/.sdk/model/target/{c.aontu → c.aon} +0 -0
- /package/project/.sdk/model/target/{clojure.aontu → clojure.aon} +0 -0
- /package/project/.sdk/model/target/{cpp.aontu → cpp.aon} +0 -0
- /package/project/.sdk/model/target/{csharp.aontu → csharp.aon} +0 -0
- /package/project/.sdk/model/target/{dart.aontu → dart.aon} +0 -0
- /package/project/.sdk/model/target/{elixir.aontu → elixir.aon} +0 -0
- /package/project/.sdk/model/target/{go-cli.aontu → go-cli.aon} +0 -0
- /package/project/.sdk/model/target/{go-mcp.aontu → go-mcp.aon} +0 -0
- /package/project/.sdk/model/target/{go.aontu → go.aon} +0 -0
- /package/project/.sdk/model/target/{java.aontu → java.aon} +0 -0
- /package/project/.sdk/model/target/{js.aontu → js.aon} +0 -0
- /package/project/.sdk/model/target/{kotlin.aontu → kotlin.aon} +0 -0
- /package/project/.sdk/model/target/{lean.aontu → lean.aon} +0 -0
- /package/project/.sdk/model/target/{lua.aontu → lua.aon} +0 -0
- /package/project/.sdk/model/target/{ocaml.aontu → ocaml.aon} +0 -0
- /package/project/.sdk/model/target/{perl.aontu → perl.aon} +0 -0
- /package/project/.sdk/model/target/{php.aontu → php.aon} +0 -0
- /package/project/.sdk/model/target/{py-data.aontu → py-data.aon} +0 -0
- /package/project/.sdk/model/target/{py.aontu → py.aon} +0 -0
- /package/project/.sdk/model/target/{rb.aontu → rb.aon} +0 -0
- /package/project/.sdk/model/target/{rust.aontu → rust.aon} +0 -0
- /package/project/.sdk/model/target/{scala.aontu → scala.aon} +0 -0
- /package/project/.sdk/model/target/{seneca-provider.aontu → seneca-provider.aon} +0 -0
- /package/project/.sdk/model/target/{swift.aontu → swift.aon} +0 -0
- /package/project/.sdk/model/target/{ts.aontu → ts.aon} +0 -0
- /package/project/.sdk/model/target/{zig.aontu → zig.aon} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Features
|
|
2
|
+
|
|
3
|
+
# Core
|
|
4
|
+
@"test.aon"
|
|
5
|
+
@"log.aon"
|
|
6
|
+
|
|
7
|
+
# Enterprise capabilities
|
|
8
|
+
@"retry.aon"
|
|
9
|
+
@"timeout.aon"
|
|
10
|
+
@"ratelimit.aon"
|
|
11
|
+
@"cache.aon"
|
|
12
|
+
@"idempotency.aon"
|
|
13
|
+
@"paging.aon"
|
|
14
|
+
@"streaming.aon"
|
|
15
|
+
@"proxy.aon"
|
|
16
|
+
@"telemetry.aon"
|
|
17
|
+
@"metrics.aon"
|
|
18
|
+
@"debug.aon"
|
|
19
|
+
@"audit.aon"
|
|
20
|
+
@"clienttrack.aon"
|
|
21
|
+
@"rbac.aon"
|
|
22
|
+
|
|
23
|
+
# Test support: network behaviour simulation
|
|
24
|
+
@"netsim.aon"
|
|
@@ -242,8 +242,15 @@ function seedRecord(e: any, idx: number): Record<string, any> {
|
|
|
242
242
|
else if (e.parents.includes(f.name)) {
|
|
243
243
|
// A nested entity's parent id must match a record the parent seeds, or
|
|
244
244
|
// the offline store answers nothing and every nested test reads as a
|
|
245
|
-
// false pass.
|
|
246
|
-
|
|
245
|
+
// false pass. Reuses parentSeed's fallback rather than f.parentEntity
|
|
246
|
+
// directly: when no entity in the model shares this key's name (the
|
|
247
|
+
// common case for a scoping param like `user_id` with no `user`
|
|
248
|
+
// entity, or a same-named response field that means something else
|
|
249
|
+
// entirely, like GitHub's `owner`), f.parentEntity is '' and seeding
|
|
250
|
+
// '0' desynced the record from every query built against the SAME
|
|
251
|
+
// key via parentSeed (parentPairs, crudTest, ...) — 0 results, or a
|
|
252
|
+
// seeded field asserted against the wrong literal.
|
|
253
|
+
out[f.name] = parentSeed(e, f.name)
|
|
247
254
|
}
|
|
248
255
|
else if ('number' === f.kind) {
|
|
249
256
|
out[f.name] = 100 * (idx + 1)
|
|
@@ -407,11 +414,20 @@ describe('${provider.fileBase}', () => {
|
|
|
407
414
|
|
|
408
415
|
`)
|
|
409
416
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
417
|
+
// Every flat entity (no parent keys), not just one "subject" — a
|
|
418
|
+
// provider with two or more flat siblings used to leave every one
|
|
419
|
+
// but the busiest untested beyond the accessor check above. A bare
|
|
420
|
+
// `list$()`/`load$(id)` call has no way to carry a parent key, so
|
|
421
|
+
// entities that need one are covered by the `nested` block below
|
|
422
|
+
// instead, with their keys filled in.
|
|
423
|
+
const flat = provider.entities.filter((e: any) => 0 === e.parents.length)
|
|
424
|
+
|
|
425
|
+
each(flat, (e: any) => {
|
|
426
|
+
if (e.cmds.includes('list')) {
|
|
427
|
+
Content(`
|
|
428
|
+
it('${e.name}-list', async () => {
|
|
413
429
|
const seneca = await makeSeneca()
|
|
414
|
-
const list = await seneca.entity('provider/${provider.lower}/${
|
|
430
|
+
const list = await seneca.entity('provider/${provider.lower}/${e.name}').list$()
|
|
415
431
|
|
|
416
432
|
assert.equal(list.length, 2)
|
|
417
433
|
|
|
@@ -420,25 +436,25 @@ describe('${provider.fileBase}', () => {
|
|
|
420
436
|
// survive into the Seneca entity.
|
|
421
437
|
assert.equal(
|
|
422
438
|
list[0].canon$({ string: true }),
|
|
423
|
-
'provider/${provider.lower}/${
|
|
439
|
+
'provider/${provider.lower}/${e.name}',
|
|
424
440
|
)
|
|
425
441
|
})
|
|
426
442
|
|
|
427
443
|
`)
|
|
428
|
-
|
|
444
|
+
}
|
|
429
445
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
it('${
|
|
446
|
+
if (e.cmds.includes('load')) {
|
|
447
|
+
Content(`
|
|
448
|
+
it('${e.name}-load', async () => {
|
|
433
449
|
const seneca = await makeSeneca()
|
|
434
450
|
const found = await seneca
|
|
435
|
-
.entity('provider/${provider.lower}/${
|
|
436
|
-
.load$('${
|
|
451
|
+
.entity('provider/${provider.lower}/${e.name}')
|
|
452
|
+
.load$('${e.name}0')
|
|
437
453
|
|
|
438
|
-
assert.equal(found.${
|
|
454
|
+
assert.equal(found.${e.idf || 'id'}, '${e.name}0')
|
|
439
455
|
assert.equal(
|
|
440
456
|
found.canon$({ string: true }),
|
|
441
|
-
'provider/${provider.lower}/${
|
|
457
|
+
'provider/${provider.lower}/${e.name}',
|
|
442
458
|
)
|
|
443
459
|
})
|
|
444
460
|
|
|
@@ -446,17 +462,18 @@ describe('${provider.fileBase}', () => {
|
|
|
446
462
|
// A 404 from a single-item read is an ordinary "not found" answer, not a
|
|
447
463
|
// failure: the provider turns it into null rather than letting the SDK
|
|
448
464
|
// throw.
|
|
449
|
-
it('${
|
|
465
|
+
it('${e.name}-load-missing', async () => {
|
|
450
466
|
const seneca = await makeSeneca()
|
|
451
467
|
const missing = await seneca
|
|
452
|
-
.entity('provider/${provider.lower}/${
|
|
453
|
-
.load$('nosuch${
|
|
468
|
+
.entity('provider/${provider.lower}/${e.name}')
|
|
469
|
+
.load$('nosuch${e.name}')
|
|
454
470
|
|
|
455
471
|
assert.equal(missing, null)
|
|
456
472
|
})
|
|
457
473
|
|
|
458
474
|
`)
|
|
459
|
-
|
|
475
|
+
}
|
|
476
|
+
})
|
|
460
477
|
|
|
461
478
|
// A nested entity cannot build its path without the parent id. That is
|
|
462
479
|
// the mistake this target exists to make impossible, so pin it.
|
|
@@ -469,17 +486,31 @@ describe('${provider.fileBase}', () => {
|
|
|
469
486
|
const pairs = e.parents
|
|
470
487
|
.map((k: string) => `${k}: '${parentSeed(e, k)}'`).join(', ')
|
|
471
488
|
|
|
472
|
-
|
|
489
|
+
// The guard is PER OP (Main's opParents), not a blanket property of
|
|
490
|
+
// the entity, so the op this test calls has to be one that actually
|
|
491
|
+
// requires `key` — hardcoding `list` assumed every nested entity's
|
|
492
|
+
// list is parent-scoped, which fails for e.g. an entity guarded on
|
|
493
|
+
// load/update/remove but whose list is unscoped (GitHub's `repo`:
|
|
494
|
+
// owner guards load, not list).
|
|
495
|
+
const guardOp = ['list', 'load', 'update', 'remove']
|
|
496
|
+
.find((op: string) => (e.opParents[op] || []).includes(key))
|
|
497
|
+
|
|
498
|
+
if (null != guardOp) {
|
|
499
|
+
const call = 'list' === guardOp ?
|
|
500
|
+
`${guardOp}$({})` : `${guardOp}$({ id: '${e.name}0' })`
|
|
501
|
+
|
|
502
|
+
Content(`
|
|
473
503
|
it('${e.name}-needs-${key}', async () => {
|
|
474
504
|
const seneca = await makeSeneca()
|
|
475
505
|
|
|
476
506
|
await assert.rejects(
|
|
477
|
-
() => seneca.entity('provider/${provider.lower}/${e.name}')
|
|
507
|
+
() => seneca.entity('provider/${provider.lower}/${e.name}').${call},
|
|
478
508
|
/${key} is required/,
|
|
479
509
|
)
|
|
480
510
|
})
|
|
481
511
|
|
|
482
512
|
`)
|
|
513
|
+
}
|
|
483
514
|
if (e.cmds.includes('list')) {
|
|
484
515
|
// Assert on the SEEDED RECORDS, not merely that an array came back.
|
|
485
516
|
// `Array.isArray` is true of the empty array, so the nested-list
|
|
@@ -4024,4 +4055,6 @@ export {
|
|
|
4024
4055
|
Workflow,
|
|
4025
4056
|
Readme,
|
|
4026
4057
|
Docs,
|
|
4058
|
+
seedRecord,
|
|
4059
|
+
parentSeed,
|
|
4027
4060
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
cmp, each,
|
|
3
3
|
File, Content, Copy, Folder,
|
|
4
4
|
entityCollection, entityOps, entityIdField, entityClassName,
|
|
5
|
-
opRequestShape, opParams, entityPath,
|
|
5
|
+
opRequestShape, opParams, ownPoint, entityPath,
|
|
6
6
|
collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
|
|
7
7
|
authorInfo, contributorList, isAuthActive, jsKey, jsProp,
|
|
8
8
|
SdkGenError,
|
|
@@ -74,12 +74,19 @@ function requiredKeys(ent: any, opname: string): string[] {
|
|
|
74
74
|
// `<entity>_id` path param to `id` besides, which is why most APIs never reach
|
|
75
75
|
// the fallback.
|
|
76
76
|
//
|
|
77
|
-
// When it does NOT answer, the record key is the LAST
|
|
78
|
-
//
|
|
79
|
-
// construction. Without this the key was simply unknown,
|
|
80
|
-
// `code` failed the `!== idf` test in opParentKeys and
|
|
81
|
-
// PARENT — `load$('SAVE20')` threw "coupon load: code
|
|
82
|
-
// loading anything, and the entity was treated as
|
|
77
|
+
// When it does NOT answer, the record key is the LAST path param of the
|
|
78
|
+
// op's own point, in PATH order — a route addresses parents first and the
|
|
79
|
+
// record last, by construction. Without this the key was simply unknown,
|
|
80
|
+
// so a param named `code` failed the `!== idf` test in opParentKeys and
|
|
81
|
+
// was classified as a PARENT — `load$('SAVE20')` threw "coupon load: code
|
|
82
|
+
// is required" instead of loading anything, and the entity was treated as
|
|
83
|
+
// nested throughout.
|
|
84
|
+
//
|
|
85
|
+
// opParams(op) is NOT the source here: it alphabetizes params for output
|
|
86
|
+
// stability, which loses path order on a 3+-param route — Airtable's
|
|
87
|
+
// record (base_id, table_id, record_id) alphabetizes with table_id last,
|
|
88
|
+
// so the old `params[params.length - 1]` picked the wrong parent as the
|
|
89
|
+
// record's own key. The point's own `parts` still has the true order.
|
|
83
90
|
function recordKey(ent: any): string {
|
|
84
91
|
const idf = entityIdField(ent)
|
|
85
92
|
if (null != idf && '' !== idf) {
|
|
@@ -88,13 +95,18 @@ function recordKey(ent: any): string {
|
|
|
88
95
|
|
|
89
96
|
for (const opname of ['load', 'remove', 'update']) {
|
|
90
97
|
const op = (ent.op || {})[opname]
|
|
91
|
-
if (null == op) {
|
|
98
|
+
if (null == op || 0 === (op.points || []).length) {
|
|
92
99
|
continue
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
const canonical = op.points.filter((pt: any) =>
|
|
103
|
+
null == (pt && pt.select && pt.select['$action']))
|
|
104
|
+
const point = ownPoint(0 < canonical.length ? canonical : op.points)
|
|
105
|
+
const parts: string[] = (point && point.parts) || []
|
|
106
|
+
const lastParam = [...parts].reverse().find((p: string) => p.startsWith('{'))
|
|
107
|
+
|
|
108
|
+
if (null != lastParam) {
|
|
109
|
+
return lastParam.slice(1, -1)
|
|
98
110
|
}
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -966,4 +978,5 @@ if ('undefined' !== typeof module) {
|
|
|
966
978
|
|
|
967
979
|
export {
|
|
968
980
|
Main,
|
|
981
|
+
recordKey,
|
|
969
982
|
}
|
|
@@ -153,6 +153,92 @@ function unwrapListData(data: any): any[] | null {
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
// GraphQL-backed op: a REST-shaped direct() call (GET, params in the URL)
|
|
157
|
+
// cannot reach it — every op, including list, synthesizes POST with the
|
|
158
|
+
// query/variables as a JSON body, not URL params (see
|
|
159
|
+
// MakeFetchDefUtility: spec.body only ever comes from an explicit `body`
|
|
160
|
+
// field, never derived from `params`). apidef already built a real, valid
|
|
161
|
+
// query or mutation document per point (point.graphql.doc, with variables
|
|
162
|
+
// declared to match), so reuse that verbatim through the SDK's own
|
|
163
|
+
// graphql() escape hatch instead of re-deriving a REST-shaped call that
|
|
164
|
+
// cannot represent one.
|
|
165
|
+
function generateDirectGraphql(
|
|
166
|
+
opname: 'load' | 'list',
|
|
167
|
+
entity: ModelEntity,
|
|
168
|
+
point: any,
|
|
169
|
+
strict: boolean,
|
|
170
|
+
) {
|
|
171
|
+
const doc: string = point.graphql.doc
|
|
172
|
+
const vars: any[] = point.graphql.vars || []
|
|
173
|
+
|
|
174
|
+
const varLine = (target: string, key: string, v: any) =>
|
|
175
|
+
` ${target}[${JSON.stringify(v.name)}] = ${key}`
|
|
176
|
+
|
|
177
|
+
const mockVarLines = vars.map((v: any, i: number) =>
|
|
178
|
+
varLine('variables', `'direct0${i + 1}'`, v)).join('\n')
|
|
179
|
+
|
|
180
|
+
const liveVarLines = vars.map((v: any) => {
|
|
181
|
+
const from = v.from || v.name
|
|
182
|
+
const key = ('id' === from ? entity.name : from.replace(/_id$/, '')) + '01'
|
|
183
|
+
return varLine('variables', `setup.idmap['${key}']`, v)
|
|
184
|
+
}).join('\n')
|
|
185
|
+
|
|
186
|
+
const liveIdKeys = vars.map((v: any) => {
|
|
187
|
+
const from = v.from || v.name
|
|
188
|
+
return ('id' === from ? entity.name : from.replace(/_id$/, '')) + '01'
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const skipMissingLine = 0 < liveIdKeys.length
|
|
192
|
+
? ` if (skipIfMissingIds(t, setup, ${JSON.stringify(liveIdKeys)})) return\n`
|
|
193
|
+
: ''
|
|
194
|
+
|
|
195
|
+
// Asserted against the OUTGOING request body (what we sent), not the
|
|
196
|
+
// mocked response — response-shape correctness is the entity-level
|
|
197
|
+
// load/list tests' job; direct/graphql only has to prove the raw path
|
|
198
|
+
// reaches the endpoint with the right method and payload.
|
|
199
|
+
const varAsserts = vars.map((_v: any, i: number) =>
|
|
200
|
+
' assert(calls[0].init.body.includes(\'direct0' + (i + 1) + '\'))\n').join('')
|
|
201
|
+
|
|
202
|
+
const offlineChecks = ` assert(result.ok === true)
|
|
203
|
+
assert(result.status === 200)
|
|
204
|
+
assert(null != result.data)
|
|
205
|
+
assert(calls.length === 1)
|
|
206
|
+
assert(calls[0].init.method === 'POST')
|
|
207
|
+
${varAsserts}`
|
|
208
|
+
|
|
209
|
+
const checks = strict ?
|
|
210
|
+
offlineChecks.replace(/^ {6}/gm, ' ').replace(/^ {4}$/gm, '') :
|
|
211
|
+
` if (setup.live) {
|
|
212
|
+
// Live mode is lenient: synthetic ids frequently fail server-side
|
|
213
|
+
// validation. Skip rather than fail when the call doesn't come back
|
|
214
|
+
// clean.
|
|
215
|
+
if (!result.ok || result.status < 200 || result.status >= 300) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
${offlineChecks} }`
|
|
220
|
+
|
|
221
|
+
Content(`
|
|
222
|
+
test('direct-${opname}-${entity.name}', async (t: any) => {
|
|
223
|
+
const setup = directSetup()
|
|
224
|
+
if (maybeSkipControl(t, 'direct', 'direct-${opname}-${entity.name}', setup.live)) return
|
|
225
|
+
${skipMissingLine} const { client, calls } = setup
|
|
226
|
+
|
|
227
|
+
const variables: any = {}
|
|
228
|
+
if (setup.live) {
|
|
229
|
+
${liveVarLines || ' // no variables'}
|
|
230
|
+
} else {
|
|
231
|
+
${mockVarLines || ' // no variables'}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result: any = await client.graphql(${JSON.stringify(doc)}, variables)
|
|
235
|
+
|
|
236
|
+
${checks}
|
|
237
|
+
})
|
|
238
|
+
`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
156
242
|
function generateDirectLoad(model: Model, entity: ModelEntity, strict: boolean) {
|
|
157
243
|
const loadOp = entity.op?.load
|
|
158
244
|
const loadPoint: ModelPoint | undefined = loadOp?.points?.[0]
|
|
@@ -161,6 +247,11 @@ function generateDirectLoad(model: Model, entity: ModelEntity, strict: boolean)
|
|
|
161
247
|
return
|
|
162
248
|
}
|
|
163
249
|
|
|
250
|
+
if ('graphql' === (loadPoint as any).kind) {
|
|
251
|
+
generateDirectGraphql('load', entity, loadPoint, strict)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
164
255
|
const allLoadParams = loadPoint.args?.params || []
|
|
165
256
|
const loadPath = normalizePathParams(loadPoint.parts || [], allLoadParams, loadPoint.rename?.param)
|
|
166
257
|
|
|
@@ -377,6 +468,11 @@ function generateDirectList(model: Model, entity: ModelEntity, strict: boolean)
|
|
|
377
468
|
return
|
|
378
469
|
}
|
|
379
470
|
|
|
471
|
+
if ('graphql' === (listPoint as any).kind) {
|
|
472
|
+
generateDirectGraphql('list', entity, listPoint, strict)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
380
476
|
const listParams = listPoint.args?.params || []
|
|
381
477
|
const listPath = normalizePathParams(listPoint.parts || [], listParams, listPoint.rename?.param)
|
|
382
478
|
|
|
@@ -11,7 +11,7 @@ clean:
|
|
|
11
11
|
rm -rf bin obj test/bin test/obj
|
|
12
12
|
|
|
13
13
|
# --- Release: tag for NuGet ------------------------------------------------
|
|
14
|
-
# NuGet publication is pending (see model/target/csharp.
|
|
14
|
+
# NuGet publication is pending (see model/target/csharp.aon publish block);
|
|
15
15
|
# publishing a version currently means pushing the tag csharp/vX.Y.Z. When the
|
|
16
16
|
# registry goes live, `dotnet pack` + `dotnet nuget push` slot in before the
|
|
17
17
|
# tag (mirroring the voxgig/struct csharp Makefile). Run headless under the
|
|
@@ -5,8 +5,28 @@ module VoxgigStruct
|
|
|
5
5
|
# --- Debug Logging Configuration ---
|
|
6
6
|
DEBUG = false
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# LAZY. The argument to a Ruby method is evaluated BEFORE the call, so
|
|
9
|
+
# `log("... #{val.inspect} ...")` built its message on every call and then
|
|
10
|
+
# threw it away, because DEBUG is false. `val` here is a node of the API
|
|
11
|
+
# model, and `inspect` serialises the whole subtree — so each property access
|
|
12
|
+
# cost O(size of the tree it was reading from), turning a walk over the model
|
|
13
|
+
# into O(n^2).
|
|
14
|
+
#
|
|
15
|
+
# Measured on gitlab-sdk (276 entities in config.options), which constructs
|
|
16
|
+
# one client per generated test:
|
|
17
|
+
#
|
|
18
|
+
# validate(options) 4.620s -> 0.039s (118x)
|
|
19
|
+
# client construction 5.790s -> 0.041s (141x)
|
|
20
|
+
# per-entity validate 0.0182s -> 0.0001s, and flat instead of climbing
|
|
21
|
+
#
|
|
22
|
+
# The whole rb suite took 7,894s against 431s for ts and 255s for php on the
|
|
23
|
+
# SAME model. Ruby was not slow; this was.
|
|
24
|
+
#
|
|
25
|
+
# A block is not evaluated unless yielded, so with DEBUG false the message
|
|
26
|
+
# now costs nothing. Call sites pass `log { "..." }`, never `log("...")`.
|
|
27
|
+
def self.log(msg = nil)
|
|
28
|
+
return unless DEBUG
|
|
29
|
+
puts "[DEBUG] #{block_given? ? yield : msg}"
|
|
10
30
|
end
|
|
11
31
|
|
|
12
32
|
# --- Helper to convert internal undefined marker to Ruby nil ---
|
|
@@ -137,33 +157,33 @@ module VoxgigStruct
|
|
|
137
157
|
# --- Internal getprop ---
|
|
138
158
|
# Returns the value if found; otherwise returns alt (default is UNDEF)
|
|
139
159
|
def self._getprop(val, key, alt = UNDEF)
|
|
140
|
-
log
|
|
160
|
+
log { "(_getprop) called with val=#{val.inspect} and key=#{key.inspect}" }
|
|
141
161
|
return alt if val.nil? || key.nil?
|
|
142
162
|
if islist(val)
|
|
143
163
|
key = (key.to_s =~ /\A\d+\z/) ? key.to_i : key
|
|
144
164
|
unless key.is_a?(Numeric) && key >= 0 && key < val.size
|
|
145
|
-
log
|
|
165
|
+
log { "(_getprop) index #{key.inspect} out of bounds; returning alt" }
|
|
146
166
|
return alt
|
|
147
167
|
end
|
|
148
168
|
result = val[key]
|
|
149
|
-
log
|
|
169
|
+
log { "(_getprop) returning #{result.inspect} from array for key #{key}" }
|
|
150
170
|
return result
|
|
151
171
|
elsif ismap(val)
|
|
152
172
|
key_str = key.to_s
|
|
153
173
|
if val.key?(key_str)
|
|
154
174
|
result = val[key_str]
|
|
155
|
-
log
|
|
175
|
+
log { "(_getprop) found key #{key_str.inspect} in hash, returning #{result.inspect}" }
|
|
156
176
|
return result
|
|
157
177
|
elsif key.is_a?(String) && val.key?(key.to_sym)
|
|
158
178
|
result = val[key.to_sym]
|
|
159
|
-
log
|
|
179
|
+
log { "(_getprop) found symbol key #{key.to_sym.inspect} in hash, returning #{result.inspect}" }
|
|
160
180
|
return result
|
|
161
181
|
else
|
|
162
|
-
log
|
|
182
|
+
log { "(_getprop) key #{key.inspect} not found; returning alt" }
|
|
163
183
|
return alt
|
|
164
184
|
end
|
|
165
185
|
else
|
|
166
|
-
log
|
|
186
|
+
log { "(_getprop) value is not a node; returning alt" }
|
|
167
187
|
alt
|
|
168
188
|
end
|
|
169
189
|
end
|
|
@@ -210,7 +230,7 @@ module VoxgigStruct
|
|
|
210
230
|
end
|
|
211
231
|
|
|
212
232
|
def self.setprop(parent, key, val = :no_val_provided)
|
|
213
|
-
log
|
|
233
|
+
log { ">>> setprop called with parent=#{parent.inspect}, key=#{key.inspect}, val=#{val.inspect}" }
|
|
214
234
|
return parent unless iskey(key)
|
|
215
235
|
if ismap(parent)
|
|
216
236
|
key_str = key.to_s
|
|
@@ -236,7 +256,7 @@ module VoxgigStruct
|
|
|
236
256
|
end
|
|
237
257
|
end
|
|
238
258
|
end
|
|
239
|
-
log
|
|
259
|
+
log { "<<< setprop result: #{parent.inspect}" }
|
|
240
260
|
parent
|
|
241
261
|
end
|
|
242
262
|
|
|
@@ -12,7 +12,7 @@ clean:
|
|
|
12
12
|
|
|
13
13
|
# --- Release: tag for the SwiftPM registry ---------------------------------
|
|
14
14
|
# SwiftPM has no upload step - publishing IS the git tag. Publication is
|
|
15
|
-
# pending (see model/target/swift.
|
|
15
|
+
# pending (see model/target/swift.aon publish block); publishing a version
|
|
16
16
|
# currently means pushing the tag swift/vX.Y.Z. Run headless under the boru key
|
|
17
17
|
# vault, which injects the GitHub token so nothing lands on disk or argv:
|
|
18
18
|
# boru vault exec --for=github=<alias> -- make publish
|
|
@@ -8,6 +8,35 @@ import { BaseFeature } from '../base/BaseFeature'
|
|
|
8
8
|
const S_NOT_FOUND = 'Not found'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
// Which param is entity X's own identifier, as opposed to a parent key —
|
|
12
|
+
// the load op's canonical point's LAST path segment, by construction (a
|
|
13
|
+
// route addresses parents first, the record last). Mirrors recordKey in
|
|
14
|
+
// sdkgen's Main_seneca-provider.ts; written again here because a template
|
|
15
|
+
// ships standalone, outside that package. A renamed id (e.g. Airtable's
|
|
16
|
+
// record_id) needs its own seeded field: matching only ever happens
|
|
17
|
+
// against the API's real param names, never a bare 'id' the API itself
|
|
18
|
+
// does not use.
|
|
19
|
+
function ownIdField(config: any, getpath: any, entityName: string): string {
|
|
20
|
+
for (const opname of ['load', 'remove', 'update']) {
|
|
21
|
+
const points = getpath(config, ['entity', entityName, 'op', opname, 'points']) || []
|
|
22
|
+
const canonical = points.filter((pt: any) =>
|
|
23
|
+
null == (pt && pt.select && pt.select['$action']))
|
|
24
|
+
const use = 0 < canonical.length ? canonical : points
|
|
25
|
+
let best = use[0]
|
|
26
|
+
for (const pt of use) {
|
|
27
|
+
if (null == pt || null == pt.parts || null == best || null == best.parts) continue
|
|
28
|
+
const ptterm = 0 < pt.parts.length && String(pt.parts[pt.parts.length - 1]).startsWith('{')
|
|
29
|
+
const bestterm = 0 < best.parts.length && String(best.parts[best.parts.length - 1]).startsWith('{')
|
|
30
|
+
if (ptterm !== bestterm ? ptterm : pt.parts.length < best.parts.length) best = pt
|
|
31
|
+
}
|
|
32
|
+
const parts: string[] = (best && best.parts) || []
|
|
33
|
+
const last = [...parts].reverse().find((p: string) => p.startsWith('{'))
|
|
34
|
+
if (null != last) return last.slice(1, -1)
|
|
35
|
+
}
|
|
36
|
+
return 'id'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
11
40
|
class TestFeature extends BaseFeature {
|
|
12
41
|
version = '0.0.1'
|
|
13
42
|
name = 'test'
|
|
@@ -30,10 +59,16 @@ class TestFeature extends BaseFeature {
|
|
|
30
59
|
|
|
31
60
|
this._client._mode = 'test'
|
|
32
61
|
|
|
62
|
+
const getpath = struct.getpath
|
|
63
|
+
|
|
33
64
|
// Ensure entity ids are correct.
|
|
34
65
|
walk(entity, (k: any, v: any, _parent: any, path: any) => {
|
|
35
66
|
if (2 === size(path)) {
|
|
36
67
|
setprop(v, 'id', k)
|
|
68
|
+
const idField = ownIdField(ctx.config, getpath, String(path[0]))
|
|
69
|
+
if ('id' !== idField) {
|
|
70
|
+
setprop(v, idField, k)
|
|
71
|
+
}
|
|
37
72
|
}
|
|
38
73
|
return v
|
|
39
74
|
})
|
|
@@ -172,6 +207,16 @@ class TestFeature extends BaseFeature {
|
|
|
172
207
|
|
|
173
208
|
const ent = clone(ctx.reqdata)
|
|
174
209
|
setprop(ent, 'id', id)
|
|
210
|
+
|
|
211
|
+
// A record created during the run needs the same real-key seeding
|
|
212
|
+
// the initial walk gives seed data (see ownIdField above) — without
|
|
213
|
+
// it, only `id` is set, and a load by the entity's own key right
|
|
214
|
+
// after create (recordKey !== 'id') finds nothing.
|
|
215
|
+
const idField = ownIdField(ctx.config, struct.getpath, getprop(op, 'entity'))
|
|
216
|
+
if ('id' !== idField && null == getprop(ent, idField)) {
|
|
217
|
+
setprop(ent, idField, id)
|
|
218
|
+
}
|
|
219
|
+
|
|
175
220
|
setprop(entmap, id, ent)
|
|
176
221
|
delprop(ent, '$KEY')
|
|
177
222
|
const out = clone(ent)
|
|
@@ -310,7 +355,8 @@ class TestFeature extends BaseFeature {
|
|
|
310
355
|
|
|
311
356
|
|
|
312
357
|
export {
|
|
313
|
-
TestFeature
|
|
358
|
+
TestFeature,
|
|
359
|
+
ownIdField,
|
|
314
360
|
}
|
|
315
361
|
|
|
316
362
|
|
package/src/action/action.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from '../types'
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
const indexEntry = (name: string) => `@"${name}.
|
|
15
|
+
const indexEntry = (name: string) => `@"${name}.aon"`
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
// An index line that is an ACTIVE include, and the name it includes — or
|
|
@@ -21,17 +21,17 @@ const indexEntry = (name: string) => `@"${name}.aontu"`
|
|
|
21
21
|
// Parsed rather than compared as a string, because both spellings around an
|
|
22
22
|
// include are legal aontu and mean opposite things:
|
|
23
23
|
//
|
|
24
|
-
// @"go.
|
|
25
|
-
// @"go.
|
|
26
|
-
// @"go.
|
|
27
|
-
// # @"go.
|
|
24
|
+
// @"go.aon" -> active, name 'go'
|
|
25
|
+
// @"go.aon" # pinned -> active, name 'go' (trailing comment)
|
|
26
|
+
// @"go.aon" -> active, name 'go' (indented)
|
|
27
|
+
// # @"go.aon" -> NOT active
|
|
28
28
|
//
|
|
29
29
|
// A substring test (what this used to be) reads the commented-out form as
|
|
30
30
|
// present, so `target add go` on a project that had switched the target off
|
|
31
31
|
// by hand appended nothing and reported success while the target stayed
|
|
32
32
|
// absent from the model. A whole-line equality test fixes that but then
|
|
33
33
|
// misses the trailing-comment form, and appends a SECOND active include.
|
|
34
|
-
const INDEX_ENTRY_RE = /^\s*@"([^"]+)\.
|
|
34
|
+
const INDEX_ENTRY_RE = /^\s*@"([^"]+)\.aon"\s*(?:#.*)?$/
|
|
35
35
|
|
|
36
36
|
function indexEntryName(line: string): string | undefined {
|
|
37
37
|
const m = line.match(INDEX_ENTRY_RE)
|
|
@@ -46,7 +46,7 @@ function hasIndexEntry(content: string, name: string): boolean {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
// Append `@"<name>.
|
|
49
|
+
// Append `@"<name>.aon"` import lines for each name not already present in
|
|
50
50
|
// the index content. Checking against the accumulating result (not the
|
|
51
51
|
// original) means duplicate names in the same call are added at most once.
|
|
52
52
|
function appendIndexEntries(content: string, names: string[]): string {
|
|
@@ -62,7 +62,7 @@ function appendIndexEntries(content: string, names: string[]): string {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
// Drop the `@"<name>.
|
|
65
|
+
// Drop the `@"<name>.aon"` line for each name — the inverse of
|
|
66
66
|
// appendIndexEntries, matching line-exact for the same reasons.
|
|
67
67
|
//
|
|
68
68
|
// Nothing calls this yet: a `remove` action is the fast-follow this exists
|
|
@@ -100,12 +100,12 @@ function parseAddNames(args: any[]): string[] {
|
|
|
100
100
|
// The current index file for each kind, which `UpdateIndex` appends to.
|
|
101
101
|
//
|
|
102
102
|
// `seed` IS THE UPGRADE PATH. A project scaffolded before a kind existed has
|
|
103
|
-
// no `model/<kind>/<kind>-index.
|
|
103
|
+
// no `model/<kind>/<kind>-index.aon` — every project alive today is in
|
|
104
104
|
// exactly that position for `docs` — and reading it unguarded made the FIRST
|
|
105
105
|
// `docs add` in any existing project fail on ENOENT before it wrote anything.
|
|
106
106
|
//
|
|
107
107
|
// Seeded per call rather than defaulted for every kind: a missing
|
|
108
|
-
// `target-index.
|
|
108
|
+
// `target-index.aon` in a scaffolded project is a broken project, and
|
|
109
109
|
// quietly recreating it would hide that. A kind the project has never used is
|
|
110
110
|
// a different thing, and only its own action knows which case it is in.
|
|
111
111
|
function loadContent(
|
|
@@ -119,7 +119,7 @@ function loadContent(
|
|
|
119
119
|
const modelfolder = Path.dirname(actx.url)
|
|
120
120
|
|
|
121
121
|
which.map((w: string) => {
|
|
122
|
-
const indexfile = Path.join(modelfolder, w, w + '-index.
|
|
122
|
+
const indexfile = Path.join(modelfolder, w, w + '-index.aon')
|
|
123
123
|
|
|
124
124
|
content[`${w}_index`] = (null != seed?.[w] && !fs.existsSync(indexfile)) ?
|
|
125
125
|
seed[w] : fs.readFileSync(indexfile, 'utf8')
|
|
@@ -136,7 +136,7 @@ function loadContent(
|
|
|
136
136
|
// indexes of the kinds that existed then. So a project scaffolded before a
|
|
137
137
|
// kind existed — which is every project alive today, for `docs` — never
|
|
138
138
|
// includes its index, and the item's model file is an orphan: it is on disk,
|
|
139
|
-
// `<kind>-index.
|
|
139
|
+
// `<kind>-index.aon` includes it, and NOTHING includes that. `main.kit.docs`
|
|
140
140
|
// is then absent from the compiled model, so `package list`, `package update`
|
|
141
141
|
// and `doctor` cannot see the item at all.
|
|
142
142
|
//
|
package/src/action/check.ts
CHANGED
|
@@ -302,7 +302,7 @@ function checkDefinition(
|
|
|
302
302
|
return found
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
// 4. Does it declare the key its FILENAME promises? `model/target/iot-go.
|
|
305
|
+
// 4. Does it declare the key its FILENAME promises? `model/target/iot-go.aon`
|
|
306
306
|
// is copied to that name and included by it, so a file still declaring
|
|
307
307
|
// `main: kit: target: go:` installs an item the consumer's model never
|
|
308
308
|
// sees — the exact mistake a package author makes copying a bundled
|
|
@@ -474,7 +474,7 @@ function checkFeatureSource(
|
|
|
474
474
|
level: 'warn', point: 'feature-source-unrecognised', file,
|
|
475
475
|
kind: 'target', name: tname,
|
|
476
476
|
note: 'tm/' + tname + ': ' + strays.map((e) => e.path).join(', ') +
|
|
477
|
-
' — named like feature source, but no `model/feature/<name>.
|
|
477
|
+
' — named like feature source, but no `model/feature/<name>.aon` ' +
|
|
478
478
|
'declares ' + strays.map((e) => e.name).join(', ') +
|
|
479
479
|
', so the trim cannot recognise them and every project gets them ' +
|
|
480
480
|
'whatever its model selects'
|