@voxgig/sdkgen-infrapack 0.0.11 → 0.0.13
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/.sdk/model/target/seneca-provider.aon +0 -122
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +2 -512
- package/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +0 -21
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -541
- package/.sdk/tm/seneca-provider/Makefile +6 -0
- package/package.json +10 -10
- package/sdkgen-package.json +2 -2
|
@@ -6,45 +6,11 @@ import {
|
|
|
6
6
|
} from '@voxgig/sdkgen'
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// same `provider` shape Main builds from the model.
|
|
12
|
-
//
|
|
13
|
-
// The tests are the reason this target is worth generating at all. A provider
|
|
14
|
-
// is thin, and the thin part is exactly where the mistakes are: a cmd that
|
|
15
|
-
// forgets a parent path param, an entity that comes back under the wrong
|
|
16
|
-
// canon, a 404 that should have been `null` and instead threw. All three are
|
|
17
|
-
// checked below, offline, against the SDK's own mock transport — so a
|
|
18
|
-
// generated provider is verified without a server.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// WHERE A FETCHED SDK CHECKOUT LIVES, inside this repo.
|
|
22
|
-
//
|
|
23
|
-
// Fixed, and the same under every layout, which is the whole point: it is
|
|
24
|
-
// what lets the generated docs, the live-test instructions and `make regen`
|
|
25
|
-
// name the SDK source without naming anyone's directory layout. Gitignored —
|
|
26
|
-
// the checkout is derived from the pin and disposable, so committing it would
|
|
27
|
-
// vendor the SDK into a repo that already depends on it.
|
|
28
|
-
//
|
|
29
|
-
// NOT a git submodule. A submodule pins a COMMIT and puts the pin in git's
|
|
30
|
-
// own plumbing, where updating it is a second repository operation and a
|
|
31
|
-
// stale one is invisible in a normal diff. The pin here is an ordinary
|
|
32
|
-
// committed file naming a TAG, regenerated from the model like everything
|
|
33
|
-
// else, so it moves with the SDK version it belongs to and shows up in review.
|
|
9
|
+
|
|
10
|
+
|
|
34
11
|
const SDK_SRC_DIR = '.sdksrc'
|
|
35
12
|
|
|
36
13
|
|
|
37
|
-
// The SDK this provider was generated from: repository, release tag, and the
|
|
38
|
-
// published package the tag corresponds to.
|
|
39
|
-
//
|
|
40
|
-
// Committed, and REGENERATED — so it cannot drift from the dependency in
|
|
41
|
-
// package.json, which comes from the same model version. The tag is
|
|
42
|
-
// `v<version>` because that is what the SDK's publish workflow cuts for its
|
|
43
|
-
// primary npm target.
|
|
44
|
-
//
|
|
45
|
-
// This is the file that makes the repo independently buildable: with it, the
|
|
46
|
-
// SDK that generates this provider can be fetched from scratch, at the exact
|
|
47
|
-
// revision that generated it, by a script that knows nothing else.
|
|
48
14
|
const SdkPin = cmp(function SdkPin(props: any) {
|
|
49
15
|
const { provider } = props
|
|
50
16
|
|
|
@@ -73,37 +39,15 @@ const SdkPin = cmp(function SdkPin(props: any) {
|
|
|
73
39
|
// Does this entity's load op have a real identifying param (path or
|
|
74
40
|
// required query), e.g. GET /result?trace_id=? A paramless GET has none.
|
|
75
41
|
function loadHasKey(e: any): boolean {
|
|
76
|
-
// FROM WHAT THE HANDLER ACTUALLY SENDS (Main's addressKeys), not from the
|
|
77
|
-
// route's shape. A route can carry parameters and still be a singleton
|
|
78
|
-
// read: github's `interaction` is `/user/interaction-limits`, and its
|
|
79
|
-
// sibling `webhook_config` reads one config per app — both are called as
|
|
80
|
-
// `load({})`, so every id hits the same record and a not-found test
|
|
81
|
-
// against them asserts the opposite of the truth. Asking the route
|
|
82
|
-
// whether it has any parameter said yes for both.
|
|
83
42
|
return true === (e.idaddressed || {}).load
|
|
84
43
|
}
|
|
85
44
|
|
|
86
45
|
|
|
87
|
-
// CAN A REMOVE DELETE WHAT A CREATE JUST MADE? A round-trip that ends by
|
|
88
|
-
// asserting the record is gone needs one that can address it.
|
|
89
|
-
//
|
|
90
|
-
// github's `action` is a tag bucket whose ops address different resources:
|
|
91
|
-
// keyed `archive_format` from its download route, removed by
|
|
92
|
-
// `hosted_runner_id` and `org_id`. The remove therefore deleted whichever
|
|
93
|
-
// record the store yielded first — usually a SEEDED one — and the round-trip
|
|
94
|
-
// failed on its own record surviving, intermittently, because the created
|
|
95
|
-
// id is random and its position in iteration order decides.
|
|
96
46
|
function removeAddresses(e: any): boolean {
|
|
97
47
|
return true === (e.idaddressed || {}).remove
|
|
98
48
|
}
|
|
99
49
|
|
|
100
50
|
|
|
101
|
-
// WOULD THIS CMD REFUSE? A cmd whose only route addresses a different
|
|
102
|
-
// resource does not send the request (Main's `misaddressed`), so a test that
|
|
103
|
-
// drives it reaches the refusal and nothing beyond — a parent-key guard, a
|
|
104
|
-
// round-trip's update leg, a plain-save assertion. Each such test is skipped
|
|
105
|
-
// here and the refusal is pinned by its own `<entity>-<cmd>-refused`.
|
|
106
|
-
// What a refusing cmd's route DOES address, for the message and the note.
|
|
107
51
|
function addressNames(e: any, cmd: string): string[] {
|
|
108
52
|
const keys = (e.opParents || {})[cmd] || []
|
|
109
53
|
return 0 < keys.length ? keys : ['nothing more specific']
|
|
@@ -115,13 +59,6 @@ function cmdRefuses(e: any, cmd: string): boolean {
|
|
|
115
59
|
}
|
|
116
60
|
|
|
117
61
|
|
|
118
|
-
// The name of the entity a parent path param addresses, or '' when the model
|
|
119
|
-
// has none of that name.
|
|
120
|
-
//
|
|
121
|
-
// From `e.parentOf`, which Main derives PER KEY. `e.parentEntity` describes
|
|
122
|
-
// only the FIRST parent, so an entity nested two levels deep had every one of
|
|
123
|
-
// its parents resolved to the innermost one — addressing the wrong record, or
|
|
124
|
-
// none.
|
|
125
62
|
function parentName(e: any, key: string): string {
|
|
126
63
|
const byKey = (e.parentOf || {})[key]
|
|
127
64
|
if (null != byKey && '' !== byKey) {
|
|
@@ -143,16 +80,6 @@ function parentSeed(e: any, key: string): string {
|
|
|
143
80
|
}
|
|
144
81
|
|
|
145
82
|
|
|
146
|
-
// `key: 'value', ` pairs for an entity's parent path params, ready to splice
|
|
147
|
-
// into an object literal. Empty for a top-level entity, so the same emitter
|
|
148
|
-
// serves both.
|
|
149
|
-
//
|
|
150
|
-
// OFFLINE the value is the seeded parent id, which exists because the seed put
|
|
151
|
-
// it there. LIVE it is a local VARIABLE, emitted as ES shorthand: a real server
|
|
152
|
-
// holds whatever records it holds, and a fixture id written into a live test is
|
|
153
|
-
// a 404 waiting to happen. That is not hypothetical — seeding the live nested
|
|
154
|
-
// create is exactly how the first version of this failed, with
|
|
155
|
-
// `create: request: 404` against a parent that only ever existed in the mock.
|
|
156
83
|
function parentPairs(e: any, live: boolean): string {
|
|
157
84
|
return e.parents
|
|
158
85
|
.map((p: string) => live ? `${p}, ` : `${p}: '${parentSeed(e, p)}', `)
|
|
@@ -185,13 +112,6 @@ function entIdLiteral(e: any, suffix: string): string {
|
|
|
185
112
|
const vals = parts.map((p: string) =>
|
|
186
113
|
e.parents.includes(p) ? parentSeed(e, p) : `${e.name}${suffix}`)
|
|
187
114
|
|
|
188
|
-
// THE SUFFIX MUST SURVIVE, or `-nosuch` names the record that exists.
|
|
189
|
-
//
|
|
190
|
-
// A part that is also a parent key takes the parent's seeded value, which
|
|
191
|
-
// ignores the suffix — and for github's repo BOTH parts are parent keys,
|
|
192
|
-
// so `entIdLiteral(e, '-nosuch')` returned `owner0/repo0`. The not-found
|
|
193
|
-
// test then loaded the seeded record and asserted it was null. The last
|
|
194
|
-
// part is the record's own key, so that is where the suffix belongs.
|
|
195
115
|
if ('' !== suffix && !vals.some((v: string) => v.endsWith(suffix))) {
|
|
196
116
|
vals[vals.length - 1] = vals[vals.length - 1] + suffix
|
|
197
117
|
}
|
|
@@ -239,12 +159,6 @@ function queryPairs(e: any, live: boolean): string {
|
|
|
239
159
|
return parentPairs(e, live)
|
|
240
160
|
}
|
|
241
161
|
|
|
242
|
-
// ONLY THE PARTS TRAVEL INSIDE THE ID. A required key that is not one of
|
|
243
|
-
// them still has to be passed, and dropping every parent because SOME of
|
|
244
|
-
// them are parts left github's api_insights_summary_stat — keyed
|
|
245
|
-
// `actor_type/actor_id`, and requiring a `min_timestamp` besides — called
|
|
246
|
-
// without the timestamp its own handler guards. Its three read tests
|
|
247
|
-
// failed on the guard rather than on anything they were written to check.
|
|
248
162
|
const rest = e.parents.filter((p: string) => !parts.includes(p))
|
|
249
163
|
|
|
250
164
|
return rest
|
|
@@ -253,50 +167,12 @@ function queryPairs(e: any, live: boolean): string {
|
|
|
253
167
|
}
|
|
254
168
|
|
|
255
169
|
|
|
256
|
-
// CAN A CREATED RECORD'S COMPOSITE ID BE REBUILT? Only if every part is
|
|
257
|
-
// recoverable, and for a composite entity that is not a given.
|
|
258
|
-
//
|
|
259
|
-
// A create supplies the parts one of two ways: as path parameters of the
|
|
260
|
-
// create route, or in the response. github's repo has NEITHER for its
|
|
261
|
-
// `repo` part — `POST /user/repos` takes no path parameters, and the
|
|
262
|
-
// response names the repository `name`, never `repo`. So there is no honest
|
|
263
|
-
// way to know the id of a repo the API just made, and a create/update/remove
|
|
264
|
-
// round-trip cannot be written against it.
|
|
265
|
-
//
|
|
266
|
-
// THIS IS A MODEL GAP, NOT A TEST TO FORCE. What is missing is a mapping
|
|
267
|
-
// from a path parameter to the response field that carries it (`repo` ->
|
|
268
|
-
// `name`); apidef knows the parameter and the field but nothing relates
|
|
269
|
-
// them. Emitting the round-trip anyway produced a 404 on the update leg that
|
|
270
|
-
// pointed at the mock rather than at the cause, so the honest thing is to
|
|
271
|
-
// leave it out and say why in the generated file.
|
|
272
|
-
//
|
|
273
|
-
// load, load-missing and the malformed-id test are all still emitted: those
|
|
274
|
-
// address an existing record, where the id comes from the caller.
|
|
275
170
|
function compositeRoundTrip(e: any): boolean {
|
|
276
171
|
const parts = idPartsOf(e)
|
|
277
172
|
if (0 === parts.length) {
|
|
278
173
|
return true
|
|
279
174
|
}
|
|
280
175
|
|
|
281
|
-
// EVERY PART PLACED, AND PLACED AT THE TOP LEVEL.
|
|
282
|
-
//
|
|
283
|
-
// Placed at all: the model must say where a response carries the part, or a
|
|
284
|
-
// created record's id cannot be rebuilt by anything.
|
|
285
|
-
//
|
|
286
|
-
// Top level: only for the OFFLINE round-trip, and only because of how this
|
|
287
|
-
// transport matches a write. It takes the keys it matches on from the
|
|
288
|
-
// request BODY, and a write's addressing parameters no longer travel there
|
|
289
|
-
// — they go in the entity match, which is what stopped them displacing a
|
|
290
|
-
// nested response field. So an update finds nothing to pin the record by.
|
|
291
|
-
//
|
|
292
|
-
// Widening the transport's key set to the point's required parameters was
|
|
293
|
-
// tried and over-constrains reads: PullEntity's basic load began matching
|
|
294
|
-
// on a parameter it had never constrained, and answered 404. The proper
|
|
295
|
-
// fix is for the transport to take a write's addressing keys from the
|
|
296
|
-
// resolved path parameters specifically, which is a change to shared
|
|
297
|
-
// machinery that wants its own validation pass.
|
|
298
|
-
//
|
|
299
|
-
// Reads, lists and removes round-trip through a nested part today.
|
|
300
176
|
const from = e.idfrom || {}
|
|
301
177
|
return parts.every((p: string) => {
|
|
302
178
|
const path = from[p]
|
|
@@ -327,18 +203,6 @@ function liveParentsResolvable(provider: any, e: any): boolean {
|
|
|
327
203
|
}
|
|
328
204
|
|
|
329
205
|
|
|
330
|
-
// A DECLARED identifier derived from an entity name.
|
|
331
|
-
//
|
|
332
|
-
// apidef canonizes an entity name to `[A-Za-z_0-9]` — `canonize` strips
|
|
333
|
-
// everything else, so hyphens and dots never reach the model and `a-b` and
|
|
334
|
-
// `a_b` arrive as the same `a_b`. The one shape that survives and is NOT a
|
|
335
|
-
// legal identifier is a LEADING DIGIT, which real resources produce:
|
|
336
|
-
// `3ds-sessions` canonizes to `3ds_session`, `2fa-tokens` to `2fa_token`.
|
|
337
|
-
//
|
|
338
|
-
// A DECLARATION cannot be bracket-quoted the way a property access can — the
|
|
339
|
-
// same constraint `guardName` in Main documents — so it is prefixed instead.
|
|
340
|
-
// Only a leading digit is touched, so every ordinary entity keeps the name it
|
|
341
|
-
// has always generated.
|
|
342
206
|
function entVar(name: string, suffix = ''): string {
|
|
343
207
|
return /^[0-9]/.test(name) ? `e_${name}${suffix}` : `${name}${suffix}`
|
|
344
208
|
}
|
|
@@ -384,19 +248,6 @@ function mutableField(e: any): string {
|
|
|
384
248
|
}
|
|
385
249
|
|
|
386
250
|
|
|
387
|
-
// A create -> load -> update -> remove round-trip for one entity.
|
|
388
|
-
//
|
|
389
|
-
// Emitted for any entity declaring BOTH save and remove, in both modes: once
|
|
390
|
-
// offline against the SDK's mock transport, once live behind the server probe.
|
|
391
|
-
// The write path is where a provider actually breaks — a save that forgets a
|
|
392
|
-
// parent key, an update that creates a second record instead of amending the
|
|
393
|
-
// first — and it was covered by nothing until this existed. The hand-written
|
|
394
|
-
// provider this target was modelled on had exactly these tests, live; dropping
|
|
395
|
-
// them on the first regeneration left every cmd.save and cmd.remove action in
|
|
396
|
-
// the generated plugin unexecuted by its own suite.
|
|
397
|
-
//
|
|
398
|
-
// The created id is never asserted to a VALUE: both the mock and a real API
|
|
399
|
-
// assign it themselves and ignore any the SDK sends.
|
|
400
251
|
function crudTest(provider: any, e: any, mode: 'offline' | 'live'): string {
|
|
401
252
|
const live = 'live' === mode
|
|
402
253
|
const pairs = parentPairs(e, live)
|
|
@@ -417,29 +268,9 @@ function crudTest(provider: any, e: any, mode: 'offline' | 'live'): string {
|
|
|
417
268
|
f.name !== idf && 'id' !== f.name && !e.parents.includes(f.name)).length ?
|
|
418
269
|
seedLiteral(e, 'crud') : ''
|
|
419
270
|
|
|
420
|
-
// A COMPOSITE RECORD MUST BE CREATED IN THE SHAPE IT COMES BACK IN.
|
|
421
|
-
//
|
|
422
|
-
// The offline transport echoes what a create sent, so a create that sends
|
|
423
|
-
// its parts flat produces a record whose id cannot be read back — `from`
|
|
424
|
-
// looks for github's owner at `owner.login` and finds a bare string. The
|
|
425
|
-
// parts therefore go in at their `from` paths, appended AFTER the field
|
|
426
|
-
// literal so they win over the flat pair the seed emitted.
|
|
427
|
-
//
|
|
428
|
-
// The record key gets its own value rather than the seed's, so a created
|
|
429
|
-
// record is distinguishable from a seeded one in the same store.
|
|
430
271
|
const idmake = 0 === idPartsOf(e).length ? '' :
|
|
431
272
|
', ' + idFromPairs(e, '-crud')
|
|
432
273
|
|
|
433
|
-
// NOT EVERY WRITABLE ENTITY IS READABLE. github's `app` declares create,
|
|
434
|
-
// update, remove and list — and no load at all, because the API offers no
|
|
435
|
-
// route that reads one app back. The round-trip read `ent.load$(...)` on
|
|
436
|
-
// nine such entities, got the null the provider correctly returns for a
|
|
437
|
-
// cmd it does not implement, and died on `loaded.id` — so the write path
|
|
438
|
-
// those tests existed to cover went unexercised.
|
|
439
|
-
//
|
|
440
|
-
// What can be checked still is: the update runs on the created entity
|
|
441
|
-
// itself, and the remove runs. What cannot be checked is stated in the
|
|
442
|
-
// file rather than quietly dropped.
|
|
443
274
|
const hasLoad = e.cmds.includes('load')
|
|
444
275
|
|
|
445
276
|
const body = hasLoad ? `${ind} try {
|
|
@@ -507,12 +338,6 @@ ${body}${ind}})
|
|
|
507
338
|
}
|
|
508
339
|
|
|
509
340
|
|
|
510
|
-
// A source literal for one field, by kind.
|
|
511
|
-
//
|
|
512
|
-
// `$ARRAY` and `$OBJECT` are in the model's sentinel vocabulary and used to
|
|
513
|
-
// fall through to the string branch, so a list field came out as
|
|
514
|
-
// `tags: 'quick-tags'` — a type-incorrect body that a validating server
|
|
515
|
-
// rejects, and a fixture that quietly stopped exercising non-scalar payloads.
|
|
516
341
|
function fieldLiteral(f: any, tag: string): string {
|
|
517
342
|
switch (f.kind) {
|
|
518
343
|
case 'number': return '12345'
|
|
@@ -550,26 +375,10 @@ function seedRecord(e: any, idx: number): Record<string, any> {
|
|
|
550
375
|
if (f.name === rkey) {
|
|
551
376
|
out[f.name] = `${e.name}${idx}`
|
|
552
377
|
}
|
|
553
|
-
// AN `id` THAT IS NOT THE ADDRESSING KEY IS SEEDED DISTINCTLY. Seeding
|
|
554
|
-
// both the same value made the offline suite unable to tell a provider
|
|
555
|
-
// that addresses records correctly from one that confuses the API's own
|
|
556
|
-
// `id` with the key its routes take — the seed agreed with either. Real
|
|
557
|
-
// GitHub never returns that: a pull has a global database `id` AND a
|
|
558
|
-
// repo-scoped `number`, and they differ.
|
|
559
378
|
else if ('id' === f.name) {
|
|
560
379
|
out[f.name] = `${e.name}-apiid-${idx}`
|
|
561
380
|
}
|
|
562
381
|
else if (e.parents.includes(f.name)) {
|
|
563
|
-
// A nested entity's parent id must match a record the parent seeds, or
|
|
564
|
-
// the offline store answers nothing and every nested test reads as a
|
|
565
|
-
// false pass. Reuses parentSeed's fallback rather than f.parentEntity
|
|
566
|
-
// directly: when no entity in the model shares this key's name (the
|
|
567
|
-
// common case for a scoping param like `user_id` with no `user`
|
|
568
|
-
// entity, or a same-named response field that means something else
|
|
569
|
-
// entirely, like GitHub's `owner`), f.parentEntity is '' and seeding
|
|
570
|
-
// '0' desynced the record from every query built against the SAME
|
|
571
|
-
// key via parentSeed (parentPairs, crudTest, ...) — 0 results, or a
|
|
572
|
-
// seeded field asserted against the wrong literal.
|
|
573
382
|
out[f.name] = parentSeed(e, f.name)
|
|
574
383
|
}
|
|
575
384
|
else if ('number' === f.kind) {
|
|
@@ -589,39 +398,12 @@ function seedRecord(e: any, idx: number): Record<string, any> {
|
|
|
589
398
|
}
|
|
590
399
|
}
|
|
591
400
|
|
|
592
|
-
// THE ADDRESSING KEY IS ALWAYS PRESENT, even when the response schema has
|
|
593
|
-
// no field of that name.
|
|
594
|
-
//
|
|
595
|
-
// The offline transport is a store, and it can only answer a request by
|
|
596
|
-
// matching the request's own parameters against a stored record
|
|
597
|
-
// (TestFeature.buildArgs). github reads an org's artifact retention from
|
|
598
|
-
// `/orgs/{org}/actions/permissions/artifact-and-log-retention`, whose body
|
|
599
|
-
// is `{days, maximum_allowed_days}` — no org anywhere in it. The loop
|
|
600
|
-
// above stamps the key only onto a field that already exists, so such a
|
|
601
|
-
// record was seeded with nothing the provider addresses it by, every
|
|
602
|
-
// offline load of it answered 404, and forty-one generated tests failed on
|
|
603
|
-
// a null they could not have avoided.
|
|
604
|
-
//
|
|
605
|
-
// This is a property of the mock, not a claim about the API: a real
|
|
606
|
-
// response need not echo the path parameter that selected it, which is
|
|
607
|
-
// why the handler carries the request's own values across into the id
|
|
608
|
-
// rather than reading them back off the body.
|
|
609
401
|
if ('' !== String(rkey) && null == out[rkey] &&
|
|
610
402
|
0 === (Array.isArray(e.idparts) ? e.idparts.length : 0)) {
|
|
611
403
|
out[rkey] = e.parents.includes(rkey) ?
|
|
612
404
|
parentSeed(e, rkey) : `${e.name}${idx}`
|
|
613
405
|
}
|
|
614
406
|
|
|
615
|
-
// THE SEED MODELS THE REAL RESPONSE, through the same `from` mapping the
|
|
616
|
-
// runtime reads. github's repo owner goes to `owner.login` and its name to
|
|
617
|
-
// `name`, because that is where the API puts them — so a record the mock
|
|
618
|
-
// returns is identifiable by exactly the code that identifies a real one.
|
|
619
|
-
//
|
|
620
|
-
// This only works because the offline transport now matches a request
|
|
621
|
-
// parameter against `id.from` as well as against its own name
|
|
622
|
-
// (TestFeature.buildArgs). Seeding this shape before that landed made
|
|
623
|
-
// every composite record unfindable: the mock looked for a field called
|
|
624
|
-
// `owner` and found an object.
|
|
625
407
|
seedIdParts(e, out, idx)
|
|
626
408
|
|
|
627
409
|
return out
|
|
@@ -706,10 +488,6 @@ module.exports = { SEED }
|
|
|
706
488
|
})
|
|
707
489
|
|
|
708
490
|
|
|
709
|
-
// The message-level spec seneca-msg-test drives. TypeScript, compiled to
|
|
710
|
-
// dist-test by test/tsconfig.json — which is also why it must exist: the
|
|
711
|
-
// shipped tsconfig has `include: ["**/*.ts"]` and tsc fails outright on a
|
|
712
|
-
// config that matches no input.
|
|
713
491
|
File({ name: 'basic.messages.ts' }, () => {
|
|
714
492
|
Content(`/* Generated by @voxgig/sdkgen. Do not edit. */
|
|
715
493
|
|
|
@@ -803,12 +581,6 @@ describe('${provider.fileBase}', () => {
|
|
|
803
581
|
|
|
804
582
|
`)
|
|
805
583
|
|
|
806
|
-
// Every flat entity (no parent keys), not just one "subject" — a
|
|
807
|
-
// provider with two or more flat siblings used to leave every one
|
|
808
|
-
// but the busiest untested beyond the accessor check above. A bare
|
|
809
|
-
// `list$()`/`load$(id)` call has no way to carry a parent key, so
|
|
810
|
-
// entities that need one are covered by the `nested` block below
|
|
811
|
-
// instead, with their keys filled in.
|
|
812
584
|
const flat = provider.entities.filter((e: any) => 0 === e.parents.length)
|
|
813
585
|
|
|
814
586
|
each(flat, (e: any) => {
|
|
@@ -873,13 +645,6 @@ describe('${provider.fileBase}', () => {
|
|
|
873
645
|
// A nested entity cannot build its path without the parent id. That is
|
|
874
646
|
// the mistake this target exists to make impossible, so pin it.
|
|
875
647
|
each(nested, (e: any) => {
|
|
876
|
-
// A COMPOSITE-KEY ENTITY HAS NO SEPARATE PARENT GUARD to pin: its
|
|
877
|
-
// parents travel inside the id, so `need_<e>_<parent>` is not
|
|
878
|
-
// emitted and there is nothing that could throw "<parent> is
|
|
879
|
-
// required". What replaces it is a malformed id, which splitid_<e>
|
|
880
|
-
// refuses by name — so pin THAT instead, and keep the property the
|
|
881
|
-
// original test was defending: an incomplete address never reaches
|
|
882
|
-
// the API.
|
|
883
648
|
if (0 < idPartsOf(e).length) {
|
|
884
649
|
const sep = null != e.idsep && '' !== String(e.idsep) ? String(e.idsep) : '/'
|
|
885
650
|
const shape = idPartsOf(e).join(sep)
|
|
@@ -893,11 +658,6 @@ describe('${provider.fileBase}', () => {
|
|
|
893
658
|
e.cmds.includes('remove' === op ? 'remove' : 'load' === op ? 'load' : 'save'))
|
|
894
659
|
|
|
895
660
|
if (null != cmd) {
|
|
896
|
-
// THE OTHER REQUIRED KEYS STILL TRAVEL. A composite id carries
|
|
897
|
-
// the parts and nothing else, so an entity that also requires a
|
|
898
|
-
// plain query key — github's api_insights_summary_stat needs a
|
|
899
|
-
// `min_timestamp` besides its `actor_type/actor_id` — tripped
|
|
900
|
-
// that guard first and this test asserted the wrong refusal.
|
|
901
661
|
const rest = queryPairs(e, false)
|
|
902
662
|
const call = 'remove' === cmd ?
|
|
903
663
|
`remove$({ ${rest}id: 'incomplete' })` :
|
|
@@ -920,23 +680,10 @@ describe('${provider.fileBase}', () => {
|
|
|
920
680
|
}
|
|
921
681
|
}
|
|
922
682
|
|
|
923
|
-
// EVERY parent key, not just the first. An entity nested two levels
|
|
924
|
-
// deep is guarded on both, so a test supplying only the alphabetically
|
|
925
|
-
// first tripped the second guard and failed on the code it was meant
|
|
926
|
-
// to be exercising.
|
|
927
683
|
const key = e.parents[0]
|
|
928
684
|
const pairs = e.parents
|
|
929
685
|
.map((k: string) => `${k}: '${parentSeed(e, k)}'`).join(', ')
|
|
930
686
|
|
|
931
|
-
// The guard is PER OP (Main's opParents), not a blanket property of
|
|
932
|
-
// the entity, so the op this test calls has to be one that actually
|
|
933
|
-
// requires `key` — hardcoding `list` assumed every nested entity's
|
|
934
|
-
// list is parent-scoped, which fails for e.g. an entity guarded on
|
|
935
|
-
// load/update/remove but whose list is unscoped (GitHub's `repo`:
|
|
936
|
-
// owner guards load, not list).
|
|
937
|
-
// AND NOT A CMD THAT REFUSES. A refusing cmd emits no guards at all
|
|
938
|
-
// — the refusal replaces them — so a test driving it asserted a
|
|
939
|
-
// "<key> is required" message that no longer exists.
|
|
940
687
|
const guardOp = ['list', 'load', 'update', 'remove']
|
|
941
688
|
.find((op: string) => (e.opParents[op] || []).includes(key) &&
|
|
942
689
|
!cmdRefuses(e, op))
|
|
@@ -945,12 +692,6 @@ describe('${provider.fileBase}', () => {
|
|
|
945
692
|
// trip, because the parents live inside the id. needs-full-id above
|
|
946
693
|
// is what pins the same property for those entities.
|
|
947
694
|
if (null != guardOp && 0 === idPartsOf(e).length) {
|
|
948
|
-
// SENECA HAS NO `update$`. The entity cmds are load$/save$/list$/
|
|
949
|
-
// remove$, and an update is a `save$` on an entity that CARRIES an
|
|
950
|
-
// id — that is the whole convention this provider is built on.
|
|
951
|
-
// Emitting `update$({id})` produced eight tests that failed with
|
|
952
|
-
// "update$ is not a function", so they asserted nothing about the
|
|
953
|
-
// guard they were written for.
|
|
954
695
|
const call =
|
|
955
696
|
'list' === guardOp ? `${guardOp}$({})` :
|
|
956
697
|
'update' === guardOp ?
|
|
@@ -970,12 +711,6 @@ describe('${provider.fileBase}', () => {
|
|
|
970
711
|
`)
|
|
971
712
|
}
|
|
972
713
|
if (e.cmds.includes('list')) {
|
|
973
|
-
// Assert on the SEEDED RECORDS, not merely that an array came back.
|
|
974
|
-
// `Array.isArray` is true of the empty array, so the nested-list
|
|
975
|
-
// test passed while proving nothing: the seed puts both of this
|
|
976
|
-
// entity's records under the same parent, so both must come back,
|
|
977
|
-
// under this plugin's canon, still carrying the parent key that
|
|
978
|
-
// addressed them.
|
|
979
714
|
Content(`
|
|
980
715
|
it('${e.name}-list', async () => {
|
|
981
716
|
const seneca = await makeSeneca()
|
|
@@ -1094,12 +829,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1094
829
|
})
|
|
1095
830
|
|
|
1096
831
|
|
|
1097
|
-
// THE REFUSAL. A cmd whose only route addresses a different resource
|
|
1098
|
-
// must not send the request — `migration`'s remove would delete a
|
|
1099
|
-
// repository's migration archive, `user`'s a GPG key, `pull`'s a
|
|
1100
|
-
// review comment, with the caller's id dropped and a successful reply.
|
|
1101
|
-
// That is the worst possible answer, so it is refused, and refused
|
|
1102
|
-
// BY NAME: the message says which key the route does not take.
|
|
1103
832
|
each(provider.entities, (e: any) => {
|
|
1104
833
|
for (const cmd of ['remove', 'update']) {
|
|
1105
834
|
if (true !== (e.idmisaddressed || {})[cmd]) {
|
|
@@ -1127,18 +856,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1127
856
|
})
|
|
1128
857
|
|
|
1129
858
|
|
|
1130
|
-
// ACTIONS — the `action$` directive.
|
|
1131
|
-
//
|
|
1132
|
-
// The test that matters most is the NEGATIVE one. A name this entity
|
|
1133
|
-
// does not have must throw, because the alternative is that the plugin
|
|
1134
|
-
// ignores the key and performs an ordinary save: a call that succeeds,
|
|
1135
|
-
// reports success, and did something else. That is exactly how GitHub's
|
|
1136
|
-
// `merge` reached its provider as an "update" — the endpoint existed,
|
|
1137
|
-
// the plugin had no way to name it, and nothing said so.
|
|
1138
|
-
//
|
|
1139
|
-
// Generated for EVERY entity, whether it has actions or not: an entity
|
|
1140
|
-
// with none is the case most likely to be typed at by mistake, and its
|
|
1141
|
-
// error is the one that names the empty set.
|
|
1142
859
|
each(provider.entities, (e: any) => {
|
|
1143
860
|
const pairs = parentPairs(e, false)
|
|
1144
861
|
const acts = e.actionList.filter((a: any) => 'save' === a.cmd)
|
|
@@ -1175,22 +892,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1175
892
|
`)
|
|
1176
893
|
}
|
|
1177
894
|
|
|
1178
|
-
// THE SILENT-DROP PIN. A save with no `action$` must still take the
|
|
1179
|
-
// canonical route: the whole mechanism is worthless if adding it
|
|
1180
|
-
// changed what an ordinary call does, and this is the assertion that
|
|
1181
|
-
// would fail if the action branch ever ran unconditionally.
|
|
1182
|
-
//
|
|
1183
|
-
// GATED ON THE ENTITY BEING ABLE TO PERFORM ONE, which is three
|
|
1184
|
-
// separate facts and was none of them. The test loads a record, edits
|
|
1185
|
-
// it and saves it back, so it needs a `load` cmd to fetch with, a
|
|
1186
|
-
// canonical `update` route to save to — an entity whose only update
|
|
1187
|
-
// point is the action has no plain save at all — and a mutable field
|
|
1188
|
-
// to change. Emitted without those it ships a red suite to a package
|
|
1189
|
-
// whose action works perfectly, which is the worst kind of generated
|
|
1190
|
-
// test: it fails for a reason that is not about the code it names.
|
|
1191
|
-
//
|
|
1192
|
-
// The ACTION tests below are not gated on any of this. They are what
|
|
1193
|
-
// this entity does have.
|
|
1194
895
|
const canPlainSave = e.cmds.includes('load') &&
|
|
1195
896
|
e.canonicalOps.includes('update') &&
|
|
1196
897
|
!cmdRefuses(e, 'update')
|
|
@@ -1219,23 +920,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1219
920
|
`)
|
|
1220
921
|
}
|
|
1221
922
|
|
|
1222
|
-
// And the POSITIVE case: a name the entity DOES have is accepted
|
|
1223
|
-
// and dispatched. `directive$` rather than `make$({ action$ })`
|
|
1224
|
-
// because make$ drops an unknown trailing-`$` key before any store
|
|
1225
|
-
// sees it — see the README's Actions section.
|
|
1226
|
-
//
|
|
1227
|
-
// WHAT THIS DOES NOT ASSERT, and why. The offline mock answers by
|
|
1228
|
-
// matching a seeded record against the parameters of the point the
|
|
1229
|
-
// SDK chose, and the seed is built for the CANONICAL route — an
|
|
1230
|
-
// action route with parameters of its own has nothing seeded to
|
|
1231
|
-
// match, so the mock's honest answer is a 404. Asserting a returned
|
|
1232
|
-
// record here would mean generating a test that fails for every API
|
|
1233
|
-
// whose actions are not shaped like its CRUD.
|
|
1234
|
-
//
|
|
1235
|
-
// The provider's own responsibility is to accept the name and route
|
|
1236
|
-
// it. That is what is asserted: whatever comes back, it is not this
|
|
1237
|
-
// plugin refusing the action. Paired with the unknown-action test
|
|
1238
|
-
// above, the two together say the map holds exactly the right names.
|
|
1239
923
|
const act = acts[0]
|
|
1240
924
|
Content(`
|
|
1241
925
|
// \`${act.action}\` is an action of \`${act.op}\`: ${act.path}
|
|
@@ -1311,15 +995,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1311
995
|
`)
|
|
1312
996
|
}
|
|
1313
997
|
|
|
1314
|
-
// The write path against a REAL server. The mock answers the shape the
|
|
1315
|
-
// SDK expects by construction; only a live run proves the request the
|
|
1316
|
-
// provider builds is one the API actually accepts — which for a nested
|
|
1317
|
-
// entity means the parent id reached the URL rather than the body.
|
|
1318
|
-
//
|
|
1319
|
-
// Emitted only when a live parent id is OBTAINABLE (see
|
|
1320
|
-
// liveParentsResolvable): against a real server the parent has to be
|
|
1321
|
-
// looked up, and an entity whose parent cannot be listed offers no
|
|
1322
|
-
// honest way to get one.
|
|
1323
998
|
each(provider.entities, (e: any) => {
|
|
1324
999
|
if (e.cmds.includes('save') && e.cmds.includes('remove') &&
|
|
1325
1000
|
liveParentsResolvable(provider, e) && compositeRoundTrip(e)) {
|
|
@@ -1332,10 +1007,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1332
1007
|
`)
|
|
1333
1008
|
}
|
|
1334
1009
|
|
|
1335
|
-
// Repository hygiene, from the @seneca/maintain dependency this package
|
|
1336
|
-
// declares. Two of its checks report a fault that is not there, because
|
|
1337
|
-
// of WHERE they run rather than what they find, so each is excluded
|
|
1338
|
-
// only in the environments that break it.
|
|
1339
1010
|
Content(`
|
|
1340
1011
|
it('maintain', async () => {
|
|
1341
1012
|
const exclude = []
|
|
@@ -1430,13 +1101,6 @@ async function makeSeneca(pluginopts) {
|
|
|
1430
1101
|
})
|
|
1431
1102
|
|
|
1432
1103
|
|
|
1433
|
-
// --- test/live.js, test/quick.js --------------------------------------------
|
|
1434
|
-
//
|
|
1435
|
-
// Manual scripts, not part of `npm test`: they need the companion server in
|
|
1436
|
-
// the SDK repo's `app/`, which is not published. Generated because the path
|
|
1437
|
-
// to that server is knowable — it is the inverse of this target's own
|
|
1438
|
-
// `output: path` — so the instruction can be exact rather than "start the
|
|
1439
|
-
// server somehow".
|
|
1440
1104
|
|
|
1441
1105
|
const Scripts = cmp(function Scripts(props: any) {
|
|
1442
1106
|
const { provider } = props
|
|
@@ -1601,16 +1265,6 @@ async function run() {
|
|
|
1601
1265
|
`)
|
|
1602
1266
|
}
|
|
1603
1267
|
|
|
1604
|
-
// The NESTED write, which is the leg worth having a manual script
|
|
1605
|
-
// for: it is the one where the parent id has to reach the URL rather
|
|
1606
|
-
// than the body, and where a provider that forgets it reports an
|
|
1607
|
-
// opaque 404 instead of saying what is missing.
|
|
1608
|
-
//
|
|
1609
|
-
// Only for a child of the record just created — then the parent id is
|
|
1610
|
-
// `id`, already in hand, and removing the child leaves the server
|
|
1611
|
-
// exactly as found. A child of anything else would need its own
|
|
1612
|
-
// lookup, which belongs in the test suite rather than in a script
|
|
1613
|
-
// whose whole point is to be readable.
|
|
1614
1268
|
const child = provider.entities.find((e: any) =>
|
|
1615
1269
|
1 === e.parents.length &&
|
|
1616
1270
|
e.parentEntity === subject.name &&
|
|
@@ -1663,7 +1317,6 @@ async function run() {
|
|
|
1663
1317
|
})
|
|
1664
1318
|
|
|
1665
1319
|
|
|
1666
|
-
// --- .github/workflows/build.yml --------------------------------------------
|
|
1667
1320
|
|
|
1668
1321
|
const Workflow = cmp(function Workflow(props: any) {
|
|
1669
1322
|
const { provider } = props
|
|
@@ -1776,27 +1429,6 @@ ${!provider.liveApp ? '' : `
|
|
|
1776
1429
|
`)
|
|
1777
1430
|
})
|
|
1778
1431
|
|
|
1779
|
-
// --- publish.yml ---------------------------------------------------
|
|
1780
|
-
//
|
|
1781
|
-
// Release on a `v*` tag push, via GitHub OIDC Trusted Publishing — no
|
|
1782
|
-
// NPM_TOKEN secret anywhere. `id-token: write` lets npm exchange a
|
|
1783
|
-
// GitHub OIDC token for a short-lived publish credential, and npm
|
|
1784
|
-
// attaches provenance automatically.
|
|
1785
|
-
//
|
|
1786
|
-
// TWO THINGS ARE LOAD-BEARING AND EASY TO GET WRONG.
|
|
1787
|
-
//
|
|
1788
|
-
// The FILENAME. npm's trusted publisher is registered against this
|
|
1789
|
-
// file's name, so renaming it breaks publishing until the npm-side
|
|
1790
|
-
// configuration is changed to match. It is publish.yml deliberately.
|
|
1791
|
-
//
|
|
1792
|
-
// `npm install`, NOT `npm ci`. A Seneca plugin does not commit its
|
|
1793
|
-
// lockfile (see .gitignore), so there is nothing for ci to install
|
|
1794
|
-
// from — it fails outright. The SDK repo commits one and uses ci; this
|
|
1795
|
-
// package cannot.
|
|
1796
|
-
//
|
|
1797
|
-
// The host framework is installed explicitly for the same reason
|
|
1798
|
-
// build.yml does it: seneca and its plugins are PEER dependencies, and
|
|
1799
|
-
// the test suite requires them directly.
|
|
1800
1432
|
File({ name: 'publish.yml' }, () => {
|
|
1801
1433
|
Content(`# Generated by @voxgig/sdkgen. Do not edit.
|
|
1802
1434
|
#
|
|
@@ -1957,13 +1589,6 @@ jobs:
|
|
|
1957
1589
|
})
|
|
1958
1590
|
|
|
1959
1591
|
|
|
1960
|
-
// --- README.md ---------------------------------------------------------------
|
|
1961
|
-
//
|
|
1962
|
-
// The heading set is NOT free: @seneca/maintain checks a Seneca plugin README
|
|
1963
|
-
// for "Quick Example", "More Examples", "Motivation", "Support", "API",
|
|
1964
|
-
// "Contributing" and "Background", and the generated `maintain` test fails
|
|
1965
|
-
// without them. That check is the reason to generate this file rather than
|
|
1966
|
-
// leave it to a maintainer.
|
|
1967
1592
|
|
|
1968
1593
|
const Readme = cmp(function Readme(props: any) {
|
|
1969
1594
|
const { provider } = props
|
|
@@ -2044,16 +1669,6 @@ await seneca.ready()
|
|
|
2044
1669
|
`)
|
|
2045
1670
|
}
|
|
2046
1671
|
if (subject.cmds.includes('load')) {
|
|
2047
|
-
// THE FIRST RUNNABLE EXAMPLE HAS TO RUN. `load$('some-id')` passes a
|
|
2048
|
-
// bare id and nothing else, but the generated handler calls
|
|
2049
|
-
// `need_<entity>_<parent>()` on every parent key before it reaches the
|
|
2050
|
-
// SDK — so for any entity that has one, the README's opening example
|
|
2051
|
-
// threw `<entity> load: <parent> is required`. Show the object form
|
|
2052
|
-
// with the parent keys the handler actually enforces; the bare-string
|
|
2053
|
-
// form stays for a parentless entity, where it is correct and shorter.
|
|
2054
|
-
// A COMPOSITE KEY IS ONE STRING, not a bag of keys. Its parents travel
|
|
2055
|
-
// inside the id, so the object form with them alongside is the shape
|
|
2056
|
-
// its own handler rejects — the example has to show the joined id.
|
|
2057
1672
|
const cparts = idPartsOf(subject)
|
|
2058
1673
|
const loadArg = 0 < cparts.length ?
|
|
2059
1674
|
`'${cparts.map((p: string) => 'some-' + p).join(
|
|
@@ -2099,9 +1714,6 @@ Each API entity is exposed as a Seneca entity under
|
|
|
2099
1714
|
| Seneca entity | Commands | Fields |
|
|
2100
1715
|
| --- | --- | --- |
|
|
2101
1716
|
`)
|
|
2102
|
-
// Fields as well as commands: a reader deciding whether this plugin
|
|
2103
|
-
// covers what they need has to know what a record CONTAINS, and the
|
|
2104
|
-
// table used to answer only half the question.
|
|
2105
1717
|
each(provider.entities, (e: any) => {
|
|
2106
1718
|
const fields = 0 === e.fields.length ? '—' :
|
|
2107
1719
|
e.fields.map((f: any) => '`' + f.name + '`').join(', ')
|
|
@@ -2124,18 +1736,6 @@ missing key, rather than failing as an opaque 404 from a half-built URL.
|
|
|
2124
1736
|
})
|
|
2125
1737
|
}
|
|
2126
1738
|
|
|
2127
|
-
// CUSTOM ACTIONS.
|
|
2128
|
-
//
|
|
2129
|
-
// apidef folds a non-CRUD verb into an ordinary op as an alternative
|
|
2130
|
-
// point, and the SDK reaches it with `$action` in the call's argument.
|
|
2131
|
-
// The `ts` target documents this in its own REFERENCE.md and the same
|
|
2132
|
-
// treatment belongs here, because the Seneca spelling is DIFFERENT
|
|
2133
|
-
// (`action$`, trailing dollar, Seneca's directive convention) and a
|
|
2134
|
-
// reader who has only ever seen the SDK's would guess wrong.
|
|
2135
|
-
//
|
|
2136
|
-
// Undocumented, this is the state the plugin was in before: a GitHub
|
|
2137
|
-
// provider with a `pull` entity and no way to merge a pull request at
|
|
2138
|
-
// all, because nothing anywhere said the endpoint existed.
|
|
2139
1739
|
const acting = provider.entities.filter((e: any) => 0 < e.actionList.length)
|
|
2140
1740
|
|
|
2141
1741
|
if (0 < acting.length) {
|
|
@@ -2337,33 +1937,6 @@ ${provider.api} API definition, against the
|
|
|
2337
1937
|
})
|
|
2338
1938
|
|
|
2339
1939
|
|
|
2340
|
-
// --- doc/tutorial.md ---------------------------------------------------------
|
|
2341
|
-
//
|
|
2342
|
-
// The Diataxis TUTORIAL: an empty folder to a working script in about fifteen
|
|
2343
|
-
// minutes. It teaches, so it is deliberately narrower than the other three
|
|
2344
|
-
// documents — one path, no alternatives, and no decisions asked of the
|
|
2345
|
-
// reader.
|
|
2346
|
-
//
|
|
2347
|
-
// Two decisions shape this component.
|
|
2348
|
-
//
|
|
2349
|
-
// FIRST, a tutorial must never ask the reader to invent a value. Every id in
|
|
2350
|
-
// the script is therefore either seeded here (offline) or read back from a
|
|
2351
|
-
// list call (live) — never a literal that only happens to exist on the
|
|
2352
|
-
// author's machine. That is also why a declared server is not by itself
|
|
2353
|
-
// enough to choose the live lesson: the primary entity must be listable, and
|
|
2354
|
-
// a nested primary entity must have a listable parent, or there is no honest
|
|
2355
|
-
// way to come by the first id. Failing that the offline lesson runs, which is
|
|
2356
|
-
// a complete tutorial in its own right rather than an apology for a missing
|
|
2357
|
-
// server.
|
|
2358
|
-
//
|
|
2359
|
-
// SECOND, the step numbers are computed rather than written, because which
|
|
2360
|
-
// steps exist depends on which cmds the model declares. `step()` counts as it
|
|
2361
|
-
// emits, and the prose refers to what a step did rather than to a number that
|
|
2362
|
-
// may not be there.
|
|
2363
|
-
//
|
|
2364
|
-
// The offline seed reuses seedRecord() — the same function behind
|
|
2365
|
-
// test/seed.js — so what the reader is told to paste has the shape the SDK
|
|
2366
|
-
// really answers with.
|
|
2367
1940
|
|
|
2368
1941
|
const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
2369
1942
|
const { provider } = props
|
|
@@ -3030,21 +2603,6 @@ the way you saw:
|
|
|
3030
2603
|
})
|
|
3031
2604
|
|
|
3032
2605
|
|
|
3033
|
-
// --- doc/how-to.md ----------------------------------------------------------
|
|
3034
|
-
//
|
|
3035
|
-
// The task-oriented quadrant of the Diataxis set: one problem per section, for
|
|
3036
|
-
// a reader who already has the plugin loaded. It instructs and does not
|
|
3037
|
-
// explain — anything that starts justifying a design choice belongs in
|
|
3038
|
-
// explanation.md and is linked to instead.
|
|
3039
|
-
//
|
|
3040
|
-
// Two decisions worth naming. First, the section list is built as data before
|
|
3041
|
-
// anything is emitted, so the table of contents and the sections themselves
|
|
3042
|
-
// are produced from the SAME guards and cannot drift: a recipe that is
|
|
3043
|
-
// suppressed because no entity declares the cmd also loses its TOC entry.
|
|
3044
|
-
// Second, every example id is the one `seedRecord` gives that entity, so the
|
|
3045
|
-
// examples here and the seed in test/seed.js agree — the offline recipe can
|
|
3046
|
-
// then be copied verbatim and the ids used in every other recipe will
|
|
3047
|
-
// actually resolve.
|
|
3048
2606
|
|
|
3049
2607
|
const DocHowto = cmp(function DocHowto(props: any) {
|
|
3050
2608
|
const { provider } = props
|
|
@@ -3101,13 +2659,6 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
3101
2659
|
|
|
3102
2660
|
const key = (k: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : `'${k}'`
|
|
3103
2661
|
|
|
3104
|
-
// SERIALISE, DO NOT COERCE. `String(value)` renders an object as
|
|
3105
|
-
// `[object Object]` and an empty array as the empty string, so a field of
|
|
3106
|
-
// either kind turned the documented create recipe into a syntax error
|
|
3107
|
-
// (`code_of_conduct: [object Object]`, `labels: ,`). Every value a seed
|
|
3108
|
-
// record can hold — string, number, boolean, array, plain object — now
|
|
3109
|
-
// emits as the JS literal it claims to be, recursively, so a reader can
|
|
3110
|
-
// copy the block and run it.
|
|
3111
2662
|
const jsval = (v: any): string => {
|
|
3112
2663
|
if (null === v || undefined === v) {
|
|
3113
2664
|
return 'null'
|
|
@@ -3711,25 +3262,6 @@ ${s.body}
|
|
|
3711
3262
|
})
|
|
3712
3263
|
|
|
3713
3264
|
|
|
3714
|
-
// --- doc/reference.md ---------------------------------------------------------
|
|
3715
|
-
//
|
|
3716
|
-
// The Diátaxis reference: information-oriented, complete, and never teaching.
|
|
3717
|
-
// Everything a caller can reach — options, canons, fields, patterns, exports,
|
|
3718
|
-
// errors, environment variables, scripts — stated once, in tables, with the
|
|
3719
|
-
// exact strings the generated source actually emits.
|
|
3720
|
-
//
|
|
3721
|
-
// Three facts here are easy to get wrong by copying a hand-written original.
|
|
3722
|
-
// The guard message carries the PUBLISHED package name, because that is what
|
|
3723
|
-
// Main interpolates (`${provider.pkgName}: <entity> <cmd>: <key> is required`).
|
|
3724
|
-
// The `sdk` block of the get:info response carries the SDK's PACKAGE name, not
|
|
3725
|
-
// its slug. And an entity whose id field is not literally `id` cannot be read
|
|
3726
|
-
// with the `load$('x')` short form at all — Seneca turns that into `{id: 'x'}`,
|
|
3727
|
-
// which the generated action does not look at — so the object form is
|
|
3728
|
-
// documented for those entities instead of the string form.
|
|
3729
|
-
//
|
|
3730
|
-
// Nothing here assumes CRUD: every table row is conditional on the cmds and
|
|
3731
|
-
// ops the model actually declares, so an API offering only create, or only
|
|
3732
|
-
// reads, documents only what it has.
|
|
3733
3265
|
|
|
3734
3266
|
const DocReference = cmp(function DocReference(props: any) {
|
|
3735
3267
|
const { provider } = props
|
|
@@ -3740,8 +3272,6 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3740
3272
|
// has nothing to select here.
|
|
3741
3273
|
const acting = provider.entities.filter((e: any) => 0 < e.actionList.length)
|
|
3742
3274
|
|
|
3743
|
-
// The entity used for worked examples: the same choice the tests and README
|
|
3744
|
-
// make, so all three documents show the same entity.
|
|
3745
3275
|
const subject = [...provider.entities]
|
|
3746
3276
|
.sort((a: any, b: any) =>
|
|
3747
3277
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
@@ -3760,8 +3290,6 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3760
3290
|
|
|
3761
3291
|
const live = '' !== provider.liveBase
|
|
3762
3292
|
|
|
3763
|
-
// test/quick.js is emitted only when the subject entity can be created and
|
|
3764
|
-
// removed again — see the Scripts cmp — so only document it when it exists.
|
|
3765
3293
|
const quick = live && subject.cmds.includes('save') && subject.cmds.includes('remove')
|
|
3766
3294
|
|
|
3767
3295
|
const canon = (e: any) => `provider/${provider.lower}/${e.name}`
|
|
@@ -4450,28 +3978,10 @@ ${quick ? 'Both scripts target' : 'It targets'} \`$${provider.ENV}_TEST_BASE\`,
|
|
|
4450
3978
|
})
|
|
4451
3979
|
|
|
4452
3980
|
|
|
4453
|
-
// --- doc/explanation.md ------------------------------------------------------
|
|
4454
|
-
//
|
|
4455
|
-
// The understanding-oriented corner of the Diátaxis set: the document someone
|
|
4456
|
-
// opens when the plugin surprised them. It DISCUSSES and never instructs, so
|
|
4457
|
-
// nothing here is a step and nothing here is a table — those belong in
|
|
4458
|
-
// tutorial.md, how-to.md and reference.md.
|
|
4459
|
-
//
|
|
4460
|
-
// The hard part of generating this one is that its subject is design reasoning,
|
|
4461
|
-
// most of which is true of EVERY provider this target emits (the entityBuilder
|
|
4462
|
-
// convention, the four-cmds-to-five-ops join, the .data() hop, the 404
|
|
4463
|
-
// translation) and only some of which depends on the model (whether any entity
|
|
4464
|
-
// is nested, whether writes exist at all, whether the API declares a server).
|
|
4465
|
-
// So the invariant prose is written once and the model-dependent sections are
|
|
4466
|
-
// guarded — an API with no nesting gets no nesting section rather than a
|
|
4467
|
-
// section explaining that it has none.
|
|
4468
3981
|
|
|
4469
3982
|
const DocExplanation = cmp(function DocExplanation(props: any) {
|
|
4470
3983
|
const { provider } = props
|
|
4471
3984
|
|
|
4472
|
-
// The entity used as the worked example throughout: fewest parent keys
|
|
4473
|
-
// (nothing to arrange around it) and the most cmds. Same choice the Tests
|
|
4474
|
-
// and Readme cmps make, so the documents agree on what they talk about.
|
|
4475
3985
|
const subject = [...provider.entities]
|
|
4476
3986
|
.sort((a: any, b: any) =>
|
|
4477
3987
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
@@ -4919,23 +4429,6 @@ order-dependent and then flaky.
|
|
|
4919
4429
|
})
|
|
4920
4430
|
|
|
4921
4431
|
|
|
4922
|
-
// --- doc/ --------------------------------------------------------------------
|
|
4923
|
-
//
|
|
4924
|
-
// The Diátaxis documentation set: an index plus the four quadrants.
|
|
4925
|
-
//
|
|
4926
|
-
// WHY THIS IS GENERATED AT ALL. Every other sdkgen target emits a single
|
|
4927
|
-
// README, and for a language SDK that is the right amount: the SDK's real
|
|
4928
|
-
// reference is its types. A Seneca provider has no types a reader can browse —
|
|
4929
|
-
// its whole interface is message patterns and entity canons, which exist only
|
|
4930
|
-
// in prose. The provider this target was modelled on carried 1100 lines of
|
|
4931
|
-
// hand-written documentation for exactly that reason, and the first
|
|
4932
|
-
// regeneration left all of it orphaned: the README's link table was gone and
|
|
4933
|
-
// nothing emitted the files it had pointed at.
|
|
4934
|
-
//
|
|
4935
|
-
// Everything here is derived from the same `provider` shape the source and the
|
|
4936
|
-
// tests are built from, so the docs cannot describe an entity the plugin does
|
|
4937
|
-
// not expose, or a cmd it does not implement — the drift that makes
|
|
4938
|
-
// hand-written provider docs untrustworthy after the second API change.
|
|
4939
4432
|
|
|
4940
4433
|
const DocIndex = cmp(function DocIndex(props: any) {
|
|
4941
4434
|
const { provider } = props
|
|
@@ -4987,9 +4480,6 @@ components, where fixing it once fixes every provider.
|
|
|
4987
4480
|
})
|
|
4988
4481
|
|
|
4989
4482
|
|
|
4990
|
-
// The whole `doc/` folder. One cmp so Main names the documentation once, and
|
|
4991
|
-
// so the folder is opened in a single place — the four quadrant components
|
|
4992
|
-
// emit a File each and know nothing about where they sit.
|
|
4993
4483
|
const Docs = cmp(function Docs(props: any) {
|
|
4994
4484
|
const { provider } = props
|
|
4995
4485
|
|