@voxgig/sdkgen-infrapack 0.0.12 → 0.0.14
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 +6 -126
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +256 -655
- package/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +0 -21
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +190 -621
- package/package.json +7 -7
- 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,20 +80,68 @@ function parentSeed(e: any, key: string): string {
|
|
|
143
80
|
}
|
|
144
81
|
|
|
145
82
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
83
|
+
// A parameter name as a local variable: an API definition can hyphenate.
|
|
84
|
+
function paramVar(p: string): string {
|
|
85
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p) ? p :
|
|
86
|
+
'p_' + p.replace(/[^A-Za-z0-9_$]/g, '_')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
function regexLiteral(s: string): string {
|
|
91
|
+
return s.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
// `key: value` for one parent, from the live variable or the seed.
|
|
96
|
+
function parentPair(e: any, p: string, live: boolean): string {
|
|
97
|
+
if (!live) {
|
|
98
|
+
return `${jsKey(p)}: '${parentSeed(e, p)}', `
|
|
99
|
+
}
|
|
100
|
+
const v = paramVar(p)
|
|
101
|
+
return v === p ? `${p}, ` : `${jsKey(p)}: ${v}, `
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
156
105
|
function parentPairs(e: any, live: boolean): string {
|
|
157
|
-
return e.parents
|
|
158
|
-
|
|
159
|
-
|
|
106
|
+
return e.parents.map((p: string) => parentPair(e, p, live)).join('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// A field the record owns: not Seneca's id, not a parent path param. The API's
|
|
111
|
+
// key is the record's own only when a create has to supply it — an
|
|
112
|
+
// API-assigned id is not the caller's to send, and a required key is.
|
|
113
|
+
function ownField(e: any, f: any): boolean {
|
|
114
|
+
if (f.name === e.rk) {
|
|
115
|
+
return true === e.rkoncreate
|
|
116
|
+
}
|
|
117
|
+
return 'id' !== f.name && !e.parents.includes(f.name)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// A field an UPDATE may change. Never the key: rewriting that addresses, or
|
|
122
|
+
// renames, a different record than the one loaded.
|
|
123
|
+
function changeField(e: any, f: any): boolean {
|
|
124
|
+
return f.name !== e.rk && ownField(e, f)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
// Where a configured `apikey` goes on the wire, from the model's security
|
|
129
|
+
// declaration. Empty for an API that declares no authentication.
|
|
130
|
+
function credentialWire(provider: any): string {
|
|
131
|
+
if (!provider.authActive) {
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
if (provider.authBasic) {
|
|
135
|
+
return `\`${provider.authName}: Basic <base64 of apikey:secret>\``
|
|
136
|
+
}
|
|
137
|
+
const prefix = '' === provider.authPrefix ? '' : provider.authPrefix + ' '
|
|
138
|
+
if ('header' === provider.authIn) {
|
|
139
|
+
return `\`${provider.authName}: ${prefix}<apikey>\``
|
|
140
|
+
}
|
|
141
|
+
if ('query' === provider.authIn) {
|
|
142
|
+
return `the \`${provider.authName}\` query parameter`
|
|
143
|
+
}
|
|
144
|
+
return `the \`${provider.authName}\` ${provider.authIn}`
|
|
160
145
|
}
|
|
161
146
|
|
|
162
147
|
|
|
@@ -185,13 +170,6 @@ function entIdLiteral(e: any, suffix: string): string {
|
|
|
185
170
|
const vals = parts.map((p: string) =>
|
|
186
171
|
e.parents.includes(p) ? parentSeed(e, p) : `${e.name}${suffix}`)
|
|
187
172
|
|
|
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
173
|
if ('' !== suffix && !vals.some((v: string) => v.endsWith(suffix))) {
|
|
196
174
|
vals[vals.length - 1] = vals[vals.length - 1] + suffix
|
|
197
175
|
}
|
|
@@ -239,64 +217,18 @@ function queryPairs(e: any, live: boolean): string {
|
|
|
239
217
|
return parentPairs(e, live)
|
|
240
218
|
}
|
|
241
219
|
|
|
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
220
|
const rest = e.parents.filter((p: string) => !parts.includes(p))
|
|
249
221
|
|
|
250
|
-
return rest
|
|
251
|
-
.map((p: string) => live ? `${p}, ` : `${p}: '${parentSeed(e, p)}', `)
|
|
252
|
-
.join('')
|
|
222
|
+
return rest.map((p: string) => parentPair(e, p, live)).join('')
|
|
253
223
|
}
|
|
254
224
|
|
|
255
225
|
|
|
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
226
|
function compositeRoundTrip(e: any): boolean {
|
|
276
227
|
const parts = idPartsOf(e)
|
|
277
228
|
if (0 === parts.length) {
|
|
278
229
|
return true
|
|
279
230
|
}
|
|
280
231
|
|
|
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
232
|
const from = e.idfrom || {}
|
|
301
233
|
return parts.every((p: string) => {
|
|
302
234
|
const path = from[p]
|
|
@@ -327,18 +259,6 @@ function liveParentsResolvable(provider: any, e: any): boolean {
|
|
|
327
259
|
}
|
|
328
260
|
|
|
329
261
|
|
|
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
262
|
function entVar(name: string, suffix = ''): string {
|
|
343
263
|
return /^[0-9]/.test(name) ? `e_${name}${suffix}` : `${name}${suffix}`
|
|
344
264
|
}
|
|
@@ -364,7 +284,7 @@ ${ind} .list\$()
|
|
|
364
284
|
|
|
365
285
|
${ind} if (0 === ${pv}.length) return t.skip('no ${pe.name} to attach a ${e.name} to')
|
|
366
286
|
|
|
367
|
-
${ind} const ${p} = ${pv}[0]
|
|
287
|
+
${ind} const ${paramVar(p)} = ${pv}[0].id
|
|
368
288
|
|
|
369
289
|
`
|
|
370
290
|
}).join('')
|
|
@@ -377,26 +297,12 @@ ${ind} const ${p} = ${pv}[0].${pe.idf || 'id'}
|
|
|
377
297
|
// is dropped rather than asserted vacuously.
|
|
378
298
|
function mutableField(e: any): string {
|
|
379
299
|
const f = (e.fields || []).find((f: any) =>
|
|
380
|
-
f
|
|
381
|
-
!e.parents.includes(f.name) && 'string' === f.kind)
|
|
300
|
+
changeField(e, f) && 'string' === f.kind)
|
|
382
301
|
|
|
383
302
|
return f ? f.name : ''
|
|
384
303
|
}
|
|
385
304
|
|
|
386
305
|
|
|
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
306
|
function crudTest(provider: any, e: any, mode: 'offline' | 'live'): string {
|
|
401
307
|
const live = 'live' === mode
|
|
402
308
|
const pairs = parentPairs(e, live)
|
|
@@ -407,39 +313,21 @@ function crudTest(provider: any, e: any, mode: 'offline' | 'live'): string {
|
|
|
407
313
|
// query and entity always spell the id `id`. The provider translates to
|
|
408
314
|
// whatever the API calls it.
|
|
409
315
|
const idf = 'id'
|
|
410
|
-
|
|
316
|
+
// The update leg needs a route that updates one record; without one a
|
|
317
|
+
// save on a loaded entity would create again.
|
|
318
|
+
const mut = e.canonicalOps.includes('update') && !cmdRefuses(e, 'update') ?
|
|
319
|
+
mutableField(e) : ''
|
|
411
320
|
|
|
412
321
|
const ind = live ? ' ' : ' '
|
|
413
322
|
const mk = live ? 'makeSeneca(liveOpts())' : 'makeSeneca()'
|
|
414
323
|
const setup = live ? liveParentSetup(provider, e, ind) : ''
|
|
415
324
|
|
|
416
|
-
const made = 0 < e.fields.filter((f: any) =>
|
|
417
|
-
f.name !== idf && 'id' !== f.name && !e.parents.includes(f.name)).length ?
|
|
325
|
+
const made = 0 < e.fields.filter((f: any) => ownField(e, f)).length ?
|
|
418
326
|
seedLiteral(e, 'crud') : ''
|
|
419
327
|
|
|
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
328
|
const idmake = 0 === idPartsOf(e).length ? '' :
|
|
431
329
|
', ' + idFromPairs(e, '-crud')
|
|
432
330
|
|
|
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
331
|
const hasLoad = e.cmds.includes('load')
|
|
444
332
|
|
|
445
333
|
const body = hasLoad ? `${ind} try {
|
|
@@ -489,8 +377,11 @@ ${ind} }
|
|
|
489
377
|
${live ? `${ind} if (!live) return t.skip(noServer())\n` : ''}${ind} const seneca = await ${mk}
|
|
490
378
|
${ind} const ent = seneca.entity('provider/${provider.lower}/${e.name}')
|
|
491
379
|
|
|
492
|
-
${setup}${ind} // Seneca's convention: an entity WITHOUT an id is a create.
|
|
493
|
-
|
|
380
|
+
${setup}${ind} // Seneca's convention: an entity WITHOUT an id is a create.${true === e.rkoncreate ?
|
|
381
|
+
` This API
|
|
382
|
+
${ind} // keys ${e.name} records by \`${e.rk}\` and the create request requires it, so
|
|
383
|
+
${ind} // it is sent and comes back as the record's id.` : ` The API
|
|
384
|
+
${ind} // assigns the id itself, so the saved record comes back with one it chose.`}
|
|
494
385
|
${ind} const made = await ent.make$({ ${pairs}${made}${idmake} }).save$()
|
|
495
386
|
|
|
496
387
|
${ind} assert.ok(null != made.${idf})
|
|
@@ -507,12 +398,6 @@ ${body}${ind}})
|
|
|
507
398
|
}
|
|
508
399
|
|
|
509
400
|
|
|
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
401
|
function fieldLiteral(f: any, tag: string): string {
|
|
517
402
|
switch (f.kind) {
|
|
518
403
|
case 'number': return '12345'
|
|
@@ -529,8 +414,7 @@ function fieldLiteral(f: any, tag: string): string {
|
|
|
529
414
|
// shares with the seed.
|
|
530
415
|
function seedLiteral(e: any, tag: string): string {
|
|
531
416
|
return (e.fields || [])
|
|
532
|
-
.filter((f: any) =>
|
|
533
|
-
f.name !== e.idf && 'id' !== f.name && !e.parents.includes(f.name))
|
|
417
|
+
.filter((f: any) => ownField(e, f))
|
|
534
418
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, tag)}`)
|
|
535
419
|
.join(', ')
|
|
536
420
|
}
|
|
@@ -550,26 +434,10 @@ function seedRecord(e: any, idx: number): Record<string, any> {
|
|
|
550
434
|
if (f.name === rkey) {
|
|
551
435
|
out[f.name] = `${e.name}${idx}`
|
|
552
436
|
}
|
|
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
437
|
else if ('id' === f.name) {
|
|
560
438
|
out[f.name] = `${e.name}-apiid-${idx}`
|
|
561
439
|
}
|
|
562
440
|
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
441
|
out[f.name] = parentSeed(e, f.name)
|
|
574
442
|
}
|
|
575
443
|
else if ('number' === f.kind) {
|
|
@@ -589,39 +457,12 @@ function seedRecord(e: any, idx: number): Record<string, any> {
|
|
|
589
457
|
}
|
|
590
458
|
}
|
|
591
459
|
|
|
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
460
|
if ('' !== String(rkey) && null == out[rkey] &&
|
|
610
461
|
0 === (Array.isArray(e.idparts) ? e.idparts.length : 0)) {
|
|
611
462
|
out[rkey] = e.parents.includes(rkey) ?
|
|
612
463
|
parentSeed(e, rkey) : `${e.name}${idx}`
|
|
613
464
|
}
|
|
614
465
|
|
|
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
466
|
seedIdParts(e, out, idx)
|
|
626
467
|
|
|
627
468
|
return out
|
|
@@ -706,10 +547,6 @@ module.exports = { SEED }
|
|
|
706
547
|
})
|
|
707
548
|
|
|
708
549
|
|
|
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
550
|
File({ name: 'basic.messages.ts' }, () => {
|
|
714
551
|
Content(`/* Generated by @voxgig/sdkgen. Do not edit. */
|
|
715
552
|
|
|
@@ -803,12 +640,6 @@ describe('${provider.fileBase}', () => {
|
|
|
803
640
|
|
|
804
641
|
`)
|
|
805
642
|
|
|
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
643
|
const flat = provider.entities.filter((e: any) => 0 === e.parents.length)
|
|
813
644
|
|
|
814
645
|
each(flat, (e: any) => {
|
|
@@ -840,7 +671,7 @@ describe('${provider.fileBase}', () => {
|
|
|
840
671
|
.entity('provider/${provider.lower}/${e.name}')
|
|
841
672
|
.load$('${entIdLiteral(e, '0')}')
|
|
842
673
|
|
|
843
|
-
assert.equal(found
|
|
674
|
+
assert.equal(found.id, '${entIdLiteral(e, '0')}')
|
|
844
675
|
assert.equal(
|
|
845
676
|
found.canon$({ string: true }),
|
|
846
677
|
'provider/${provider.lower}/${e.name}',
|
|
@@ -873,13 +704,6 @@ describe('${provider.fileBase}', () => {
|
|
|
873
704
|
// A nested entity cannot build its path without the parent id. That is
|
|
874
705
|
// the mistake this target exists to make impossible, so pin it.
|
|
875
706
|
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
707
|
if (0 < idPartsOf(e).length) {
|
|
884
708
|
const sep = null != e.idsep && '' !== String(e.idsep) ? String(e.idsep) : '/'
|
|
885
709
|
const shape = idPartsOf(e).join(sep)
|
|
@@ -888,16 +712,11 @@ describe('${provider.fileBase}', () => {
|
|
|
888
712
|
// syntax error ("Invalid regular expression flags"). Escape every
|
|
889
713
|
// regex metacharacter, not just the slash, so a future separator
|
|
890
714
|
// cannot reintroduce this.
|
|
891
|
-
const shapeRe = shape
|
|
715
|
+
const shapeRe = regexLiteral(shape)
|
|
892
716
|
const cmd = ['load', 'remove', 'update'].find((op: string) =>
|
|
893
717
|
e.cmds.includes('remove' === op ? 'remove' : 'load' === op ? 'load' : 'save'))
|
|
894
718
|
|
|
895
719
|
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
720
|
const rest = queryPairs(e, false)
|
|
902
721
|
const call = 'remove' === cmd ?
|
|
903
722
|
`remove$({ ${rest}id: 'incomplete' })` :
|
|
@@ -920,23 +739,10 @@ describe('${provider.fileBase}', () => {
|
|
|
920
739
|
}
|
|
921
740
|
}
|
|
922
741
|
|
|
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
742
|
const key = e.parents[0]
|
|
928
743
|
const pairs = e.parents
|
|
929
|
-
.map((k: string) => `${k}: '${parentSeed(e, k)}'`).join(', ')
|
|
930
|
-
|
|
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.
|
|
744
|
+
.map((k: string) => `${jsKey(k)}: '${parentSeed(e, k)}'`).join(', ')
|
|
745
|
+
|
|
940
746
|
const guardOp = ['list', 'load', 'update', 'remove']
|
|
941
747
|
.find((op: string) => (e.opParents[op] || []).includes(key) &&
|
|
942
748
|
!cmdRefuses(e, op))
|
|
@@ -945,12 +751,6 @@ describe('${provider.fileBase}', () => {
|
|
|
945
751
|
// trip, because the parents live inside the id. needs-full-id above
|
|
946
752
|
// is what pins the same property for those entities.
|
|
947
753
|
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
754
|
const call =
|
|
955
755
|
'list' === guardOp ? `${guardOp}$({})` :
|
|
956
756
|
'update' === guardOp ?
|
|
@@ -963,19 +763,13 @@ describe('${provider.fileBase}', () => {
|
|
|
963
763
|
|
|
964
764
|
await assert.rejects(
|
|
965
765
|
() => seneca.entity('provider/${provider.lower}/${e.name}').${call},
|
|
966
|
-
/${key} is required/,
|
|
766
|
+
/${regexLiteral(key)} is required/,
|
|
967
767
|
)
|
|
968
768
|
})
|
|
969
769
|
|
|
970
770
|
`)
|
|
971
771
|
}
|
|
972
772
|
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
773
|
Content(`
|
|
980
774
|
it('${e.name}-list', async () => {
|
|
981
775
|
const seneca = await makeSeneca()
|
|
@@ -990,7 +784,7 @@ describe('${provider.fileBase}', () => {
|
|
|
990
784
|
)
|
|
991
785
|
${0 < idPartsOf(e).length ?
|
|
992
786
|
`assert.equal(list[0].id, '${entIdLiteral(e, '0')}')` :
|
|
993
|
-
`assert.equal(list[0]
|
|
787
|
+
`assert.equal(${jsProp('list[0]', key)}, '${parentSeed(e, key)}')`}
|
|
994
788
|
})
|
|
995
789
|
|
|
996
790
|
`)
|
|
@@ -1035,7 +829,16 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1035
829
|
// transport implements create/update/remove, so this needs no server.
|
|
1036
830
|
each(provider.entities, (e: any) => {
|
|
1037
831
|
if (e.cmds.includes('save') && e.cmds.includes('remove')) {
|
|
1038
|
-
if (
|
|
832
|
+
if (!e.canonicalOps.includes('create')) {
|
|
833
|
+
Content(`
|
|
834
|
+
// NO ${e.name} create/update/remove round-trip: THIS API HAS NO CREATE
|
|
835
|
+
// ROUTE FOR A ${e.name}, so there is no record of this test's own to
|
|
836
|
+
// update and remove. The update and remove cmds are still exercised
|
|
837
|
+
// through the guard and refusal tests above.
|
|
838
|
+
|
|
839
|
+
`)
|
|
840
|
+
}
|
|
841
|
+
else if (compositeRoundTrip(e) && removeAddresses(e) &&
|
|
1039
842
|
!cmdRefuses(e, 'update')) {
|
|
1040
843
|
Content(`
|
|
1041
844
|
` + crudTest(provider, e, 'offline'))
|
|
@@ -1094,12 +897,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1094
897
|
})
|
|
1095
898
|
|
|
1096
899
|
|
|
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
900
|
each(provider.entities, (e: any) => {
|
|
1104
901
|
for (const cmd of ['remove', 'update']) {
|
|
1105
902
|
if (true !== (e.idmisaddressed || {})[cmd]) {
|
|
@@ -1127,18 +924,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1127
924
|
})
|
|
1128
925
|
|
|
1129
926
|
|
|
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
927
|
each(provider.entities, (e: any) => {
|
|
1143
928
|
const pairs = parentPairs(e, false)
|
|
1144
929
|
const acts = e.actionList.filter((a: any) => 'save' === a.cmd)
|
|
@@ -1150,7 +935,7 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1150
935
|
|
|
1151
936
|
await assert.rejects(
|
|
1152
937
|
() => seneca.entity('provider/${provider.lower}/${e.name}')
|
|
1153
|
-
.make$({ ${pairs}id: '${e
|
|
938
|
+
.make$({ ${pairs}id: '${entIdLiteral(e, '0')}' })
|
|
1154
939
|
.directive$({ action$: 'no_such_action' })
|
|
1155
940
|
.save$(),
|
|
1156
941
|
/action\\$ "no_such_action" is not an action/,
|
|
@@ -1175,22 +960,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1175
960
|
`)
|
|
1176
961
|
}
|
|
1177
962
|
|
|
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
963
|
const canPlainSave = e.cmds.includes('load') &&
|
|
1195
964
|
e.canonicalOps.includes('update') &&
|
|
1196
965
|
!cmdRefuses(e, 'update')
|
|
@@ -1219,23 +988,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1219
988
|
`)
|
|
1220
989
|
}
|
|
1221
990
|
|
|
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
991
|
const act = acts[0]
|
|
1240
992
|
Content(`
|
|
1241
993
|
// \`${act.action}\` is an action of \`${act.op}\`: ${act.path}
|
|
@@ -1245,7 +997,7 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1245
997
|
|
|
1246
998
|
try {
|
|
1247
999
|
await seneca.entity('provider/${provider.lower}/${e.name}')
|
|
1248
|
-
.make$({ ${pairs}id: '${e
|
|
1000
|
+
.make$({ ${pairs}id: '${entIdLiteral(e, '0')}' })
|
|
1249
1001
|
.directive$({ action$: '${act.action}' })
|
|
1250
1002
|
.save$()
|
|
1251
1003
|
}
|
|
@@ -1293,17 +1045,25 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1293
1045
|
|
|
1294
1046
|
`)
|
|
1295
1047
|
}
|
|
1296
|
-
|
|
1048
|
+
// A nested subject needs its parent ids from the server, and a
|
|
1049
|
+
// composite one an id built from them; neither is available to a
|
|
1050
|
+
// literal, so the missing-record read is emitted only where it can
|
|
1051
|
+
// address something.
|
|
1052
|
+
if (subject.cmds.includes('load') && 0 === idPartsOf(subject).length &&
|
|
1053
|
+
liveParentsResolvable(provider, subject)) {
|
|
1054
|
+
const missing = 0 === subject.parents.length ?
|
|
1055
|
+
`'nosuch${subject.name}'` :
|
|
1056
|
+
`{ ${queryPairs(subject, true)}id: 'nosuch${subject.name}' }`
|
|
1297
1057
|
Content(` // A read of something that is not there is \`null\`, live as well as
|
|
1298
1058
|
// offline: the provider's 404 handling is the same code path either way.
|
|
1299
1059
|
it('${subject.name}-load-missing', async (t) => {
|
|
1300
1060
|
if (!live) return t.skip(noServer())
|
|
1301
1061
|
const seneca = await makeSeneca(liveOpts())
|
|
1302
1062
|
|
|
1303
|
-
assert.equal(
|
|
1063
|
+
${liveParentSetup(provider, subject, ' ')} assert.equal(
|
|
1304
1064
|
await seneca
|
|
1305
1065
|
.entity('provider/${provider.lower}/${subject.name}')
|
|
1306
|
-
.load$(
|
|
1066
|
+
.load$(${missing}),
|
|
1307
1067
|
null,
|
|
1308
1068
|
)
|
|
1309
1069
|
})
|
|
@@ -1311,17 +1071,9 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1311
1071
|
`)
|
|
1312
1072
|
}
|
|
1313
1073
|
|
|
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
1074
|
each(provider.entities, (e: any) => {
|
|
1324
1075
|
if (e.cmds.includes('save') && e.cmds.includes('remove') &&
|
|
1076
|
+
e.canonicalOps.includes('create') &&
|
|
1325
1077
|
liveParentsResolvable(provider, e) && compositeRoundTrip(e)) {
|
|
1326
1078
|
Content(crudTest(provider, e, 'live'))
|
|
1327
1079
|
}
|
|
@@ -1332,10 +1084,6 @@ ${!loadHasKey(e) ? '' : `
|
|
|
1332
1084
|
`)
|
|
1333
1085
|
}
|
|
1334
1086
|
|
|
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
1087
|
Content(`
|
|
1340
1088
|
it('maintain', async () => {
|
|
1341
1089
|
const exclude = []
|
|
@@ -1430,13 +1178,6 @@ async function makeSeneca(pluginopts) {
|
|
|
1430
1178
|
})
|
|
1431
1179
|
|
|
1432
1180
|
|
|
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
1181
|
|
|
1441
1182
|
const Scripts = cmp(function Scripts(props: any) {
|
|
1442
1183
|
const { provider } = props
|
|
@@ -1513,7 +1254,7 @@ async function run() {
|
|
|
1513
1254
|
Content(` // ${e.name}: needs ${e.parents.join(', ')}; no listable parent to take
|
|
1514
1255
|
// one from, so supply it yourself:
|
|
1515
1256
|
// await seneca.entity('provider/${provider.lower}/${e.name}')
|
|
1516
|
-
// .list$({ ${e.parents.map((k: string) => `${k}: '...'`).join(', ')} })
|
|
1257
|
+
// .list$({ ${e.parents.map((k: string) => `${jsKey(k)}: '...'`).join(', ')} })
|
|
1517
1258
|
|
|
1518
1259
|
`)
|
|
1519
1260
|
return
|
|
@@ -1527,7 +1268,7 @@ async function run() {
|
|
|
1527
1268
|
if (0 < ${parent.name}s.length) {
|
|
1528
1269
|
console.log('${e.name.toUpperCase()}', await seneca
|
|
1529
1270
|
.entity('provider/${provider.lower}/${e.name}')
|
|
1530
|
-
.list$({ ${key}: ${parent.name}s[0]
|
|
1271
|
+
.list$({ ${jsKey(key)}: ${parent.name}s[0].id }))
|
|
1531
1272
|
}
|
|
1532
1273
|
|
|
1533
1274
|
`)
|
|
@@ -1540,10 +1281,8 @@ async function run() {
|
|
|
1540
1281
|
// The write cycle, kept separate: it MUTATES the server, so it is not
|
|
1541
1282
|
// something to run by reflex. It cleans up after itself.
|
|
1542
1283
|
if (subject.cmds.includes('save') && subject.cmds.includes('remove')) {
|
|
1543
|
-
const idf =
|
|
1544
|
-
const writable = subject.fields
|
|
1545
|
-
.filter((f: any) => f.name !== idf && f.name !== 'id')
|
|
1546
|
-
.filter((f: any) => !subject.parents.includes(f.name))
|
|
1284
|
+
const idf = 'id'
|
|
1285
|
+
const writable = subject.fields.filter((f: any) => ownField(subject, f))
|
|
1547
1286
|
|
|
1548
1287
|
const make = writable
|
|
1549
1288
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, 'quick')}`)
|
|
@@ -1567,7 +1306,9 @@ run()
|
|
|
1567
1306
|
async function run() {
|
|
1568
1307
|
const seneca = await makeSeneca()
|
|
1569
1308
|
|
|
1570
|
-
// Create:
|
|
1309
|
+
// Create: ${true === subject.rkoncreate ?
|
|
1310
|
+
`this API keys ${subject.name} records by \`${subject.rk}\` and the create\n // request requires it, so it is sent and comes back as the id.` :
|
|
1311
|
+
'the API assigns the id, so none is supplied here.'}
|
|
1571
1312
|
let ${subject.name} = await seneca
|
|
1572
1313
|
.entity('provider/${provider.lower}/${subject.name}')
|
|
1573
1314
|
.make$({ ${make} })
|
|
@@ -1580,8 +1321,8 @@ async function run() {
|
|
|
1580
1321
|
`)
|
|
1581
1322
|
// Change something an assertion could SEE. A container field would be
|
|
1582
1323
|
// rewritten to the same empty literal, which demonstrates nothing.
|
|
1583
|
-
const upd = writable.find((f: any) =>
|
|
1584
|
-
'string' === f.kind || 'number' === f.kind) || null
|
|
1324
|
+
const upd = writable.find((f: any) => changeField(subject, f) &&
|
|
1325
|
+
('string' === f.kind || 'number' === f.kind)) || null
|
|
1585
1326
|
|
|
1586
1327
|
if (subject.ops.includes('update') && null != upd) {
|
|
1587
1328
|
const f = upd
|
|
@@ -1601,16 +1342,6 @@ async function run() {
|
|
|
1601
1342
|
`)
|
|
1602
1343
|
}
|
|
1603
1344
|
|
|
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
1345
|
const child = provider.entities.find((e: any) =>
|
|
1615
1346
|
1 === e.parents.length &&
|
|
1616
1347
|
e.parentEntity === subject.name &&
|
|
@@ -1618,10 +1349,9 @@ async function run() {
|
|
|
1618
1349
|
|
|
1619
1350
|
if (null != child) {
|
|
1620
1351
|
const ckey = child.parents[0]
|
|
1621
|
-
const cidf =
|
|
1352
|
+
const cidf = 'id'
|
|
1622
1353
|
const cmake = (child.fields || [])
|
|
1623
|
-
.filter((f: any) =>
|
|
1624
|
-
f.name !== cidf && 'id' !== f.name && !child.parents.includes(f.name))
|
|
1354
|
+
.filter((f: any) => ownField(child, f))
|
|
1625
1355
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, 'quick')}`)
|
|
1626
1356
|
.join(', ')
|
|
1627
1357
|
|
|
@@ -1629,13 +1359,13 @@ async function run() {
|
|
|
1629
1359
|
// goes under the ${subject.name} just created — and comes back off again.
|
|
1630
1360
|
const ${child.name} = await seneca
|
|
1631
1361
|
.entity('provider/${provider.lower}/${child.name}')
|
|
1632
|
-
.make$({ ${ckey}: id${'' === cmake ? '' : ', ' + cmake} })
|
|
1362
|
+
.make$({ ${jsKey(ckey)}: id${'' === cmake ? '' : ', ' + cmake} })
|
|
1633
1363
|
.save$()
|
|
1634
1364
|
console.log('${child.name.toUpperCase()} CREATED', ${child.name})
|
|
1635
1365
|
|
|
1636
1366
|
await seneca
|
|
1637
1367
|
.entity('provider/${provider.lower}/${child.name}')
|
|
1638
|
-
.remove$({ ${ckey}: id, ${cidf}: ${child.name}.${cidf} })
|
|
1368
|
+
.remove$({ ${jsKey(ckey)}: id, ${cidf}: ${child.name}.${cidf} })
|
|
1639
1369
|
console.log('${child.name.toUpperCase()} REMOVED')
|
|
1640
1370
|
|
|
1641
1371
|
`)
|
|
@@ -1663,7 +1393,6 @@ async function run() {
|
|
|
1663
1393
|
})
|
|
1664
1394
|
|
|
1665
1395
|
|
|
1666
|
-
// --- .github/workflows/build.yml --------------------------------------------
|
|
1667
1396
|
|
|
1668
1397
|
const Workflow = cmp(function Workflow(props: any) {
|
|
1669
1398
|
const { provider } = props
|
|
@@ -1765,38 +1494,11 @@ ${!provider.liveApp ? '' : `
|
|
|
1765
1494
|
`}
|
|
1766
1495
|
- run: npm install${provider.sdkInstallFlag}
|
|
1767
1496
|
|
|
1768
|
-
# The Seneca host framework is a PEER dependency, so the test suite needs
|
|
1769
|
-
# it installed explicitly. --no-save keeps npm from rewriting the peer
|
|
1770
|
-
# ranges in package.json to carets on what it happened to resolve, which
|
|
1771
|
-
# would have the build testing a manifest the repo never authored.
|
|
1772
|
-
- run: npm i --no-save seneca seneca-entity seneca-promisify @seneca/provider @seneca/env
|
|
1773
|
-
|
|
1774
1497
|
- run: npm run build --if-present
|
|
1775
1498
|
- run: npm test
|
|
1776
1499
|
`)
|
|
1777
1500
|
})
|
|
1778
1501
|
|
|
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
1502
|
File({ name: 'publish.yml' }, () => {
|
|
1801
1503
|
Content(`# Generated by @voxgig/sdkgen. Do not edit.
|
|
1802
1504
|
#
|
|
@@ -1864,16 +1566,6 @@ jobs:
|
|
|
1864
1566
|
# install, not ci: this package does not commit a lockfile.
|
|
1865
1567
|
- run: npm install${provider.sdkInstallFlag}
|
|
1866
1568
|
|
|
1867
|
-
# The Seneca host framework is a PEER dependency, so the test suite
|
|
1868
|
-
# needs it installed explicitly.
|
|
1869
|
-
#
|
|
1870
|
-
# --no-save IS LOAD-BEARING. Without it npm rewrites the peer ranges in
|
|
1871
|
-
# package.json to carets on whatever it resolved, and a later publish
|
|
1872
|
-
# ships that rewritten manifest — so an authored \`>=26\` reaches
|
|
1873
|
-
# consumers as \`^28.1.0\` and the package refuses to install for anyone
|
|
1874
|
-
# on a newer major. The repo looks fine; only the artifact is narrowed.
|
|
1875
|
-
- run: npm i --no-save seneca seneca-entity seneca-promisify @seneca/provider @seneca/env
|
|
1876
|
-
|
|
1877
1569
|
- run: npm run build
|
|
1878
1570
|
- run: npm test
|
|
1879
1571
|
|
|
@@ -1957,13 +1649,6 @@ jobs:
|
|
|
1957
1649
|
})
|
|
1958
1650
|
|
|
1959
1651
|
|
|
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
1652
|
|
|
1968
1653
|
const Readme = cmp(function Readme(props: any) {
|
|
1969
1654
|
const { provider } = props
|
|
@@ -2044,23 +1729,13 @@ await seneca.ready()
|
|
|
2044
1729
|
`)
|
|
2045
1730
|
}
|
|
2046
1731
|
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
1732
|
const cparts = idPartsOf(subject)
|
|
2058
1733
|
const loadArg = 0 < cparts.length ?
|
|
2059
1734
|
`'${cparts.map((p: string) => 'some-' + p).join(
|
|
2060
1735
|
null != subject.idsep && '' !== String(subject.idsep) ?
|
|
2061
1736
|
String(subject.idsep) : '/')}'` :
|
|
2062
1737
|
0 === subject.parents.length ? `'some-id'` :
|
|
2063
|
-
`{ ` + subject.parents.map((p: string) => `${p}: 'some-${p}'`).join(', ') +
|
|
1738
|
+
`{ ` + subject.parents.map((p: string) => `${jsKey(p)}: 'some-${p}'`).join(', ') +
|
|
2064
1739
|
`, id: 'some-id' }`
|
|
2065
1740
|
Content(`const ${subject.name} = await seneca
|
|
2066
1741
|
.entity('provider/${provider.lower}/${subject.name}').load$(${loadArg})
|
|
@@ -2099,9 +1774,6 @@ Each API entity is exposed as a Seneca entity under
|
|
|
2099
1774
|
| Seneca entity | Commands | Fields |
|
|
2100
1775
|
| --- | --- | --- |
|
|
2101
1776
|
`)
|
|
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
1777
|
each(provider.entities, (e: any) => {
|
|
2106
1778
|
const fields = 0 === e.fields.length ? '—' :
|
|
2107
1779
|
e.fields.map((f: any) => '`' + f.name + '`').join(', ')
|
|
@@ -2124,18 +1796,6 @@ missing key, rather than failing as an opaque 404 from a half-built URL.
|
|
|
2124
1796
|
})
|
|
2125
1797
|
}
|
|
2126
1798
|
|
|
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
1799
|
const acting = provider.entities.filter((e: any) => 0 < e.actionList.length)
|
|
2140
1800
|
|
|
2141
1801
|
if (0 < acting.length) {
|
|
@@ -2337,33 +1997,6 @@ ${provider.api} API definition, against the
|
|
|
2337
1997
|
})
|
|
2338
1998
|
|
|
2339
1999
|
|
|
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
2000
|
|
|
2368
2001
|
const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
2369
2002
|
const { provider } = props
|
|
@@ -2386,7 +2019,7 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2386
2019
|
.sort((a: any, b: any) =>
|
|
2387
2020
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
2388
2021
|
|
|
2389
|
-
const idf =
|
|
2022
|
+
const idf = 'id'
|
|
2390
2023
|
const subjParent = 0 < subject.parents.length ?
|
|
2391
2024
|
entOf(subject.parentEntity) : null
|
|
2392
2025
|
|
|
@@ -2420,19 +2053,14 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2420
2053
|
|
|
2421
2054
|
// The value seedRecord() gives a parent key, so a query written here finds
|
|
2422
2055
|
// the seeded record instead of quietly matching nothing.
|
|
2423
|
-
const seedParentVal = (e: any, k: string) =>
|
|
2424
|
-
const f = (e.fields || []).find((f: any) => f.name === k)
|
|
2425
|
-
const pe = (null != f && '' !== f.parentEntity) ? f.parentEntity :
|
|
2426
|
-
(k === e.parents[0] ? (e.parentEntity || '') : '')
|
|
2427
|
-
return `${pe}0`
|
|
2428
|
-
}
|
|
2056
|
+
const seedParentVal = (e: any, k: string) => parentSeed(e, k)
|
|
2429
2057
|
|
|
2430
2058
|
// A seed record guaranteed to carry its id and its parent keys.
|
|
2431
2059
|
// seedRecord() emits only the fields the model marks required, and a record
|
|
2432
2060
|
// missing its parent key is invisible to the very query this lesson makes.
|
|
2433
2061
|
const demoRecord = (e: any, idx: number) => {
|
|
2434
2062
|
const rec: any = seedRecord(e, idx)
|
|
2435
|
-
const eidf = e.
|
|
2063
|
+
const eidf = e.rk || 'id'
|
|
2436
2064
|
if (null == rec[eidf]) {
|
|
2437
2065
|
rec[eidf] = `${e.name}${idx}`
|
|
2438
2066
|
}
|
|
@@ -2482,13 +2110,13 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2482
2110
|
const ${plural(subjParent.name)} = await seneca
|
|
2483
2111
|
.entity('${canon(subjParent.name)}')
|
|
2484
2112
|
.list$()
|
|
2485
|
-
const ${ident(subject.parents[0])} = ${plural(subjParent.name)}[0]
|
|
2113
|
+
const ${ident(subject.parents[0])} = ${plural(subjParent.name)}[0].id
|
|
2486
2114
|
|
|
2487
2115
|
` : ''
|
|
2488
2116
|
|
|
2489
2117
|
// Fields worth printing, and worth writing: not the id, not a parent key.
|
|
2490
|
-
const plainFields = subject.fields.filter((f: any) =>
|
|
2491
|
-
|
|
2118
|
+
const plainFields = subject.fields.filter((f: any) => ownField(subject, f))
|
|
2119
|
+
const changeFields = plainFields.filter((f: any) => changeField(subject, f))
|
|
2492
2120
|
const shown = plainFields.slice(0, 2)
|
|
2493
2121
|
const litval = (f: any, alt: boolean) =>
|
|
2494
2122
|
'number' === f.kind ? (alt ? '4321' : '1234') :
|
|
@@ -2504,7 +2132,8 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2504
2132
|
// Creating a record with nothing in it teaches nothing, so the write step
|
|
2505
2133
|
// needs at least one field the caller actually supplies.
|
|
2506
2134
|
const canWrite = subject.cmds.includes('save') && 0 < plainFields.length
|
|
2507
|
-
const canUpdate = canWrite && subject.ops.includes('update')
|
|
2135
|
+
const canUpdate = canWrite && subject.ops.includes('update') &&
|
|
2136
|
+
0 < changeFields.length
|
|
2508
2137
|
const canRemove = canWrite && subject.cmds.includes('remove')
|
|
2509
2138
|
|
|
2510
2139
|
const cmdList = subject.cmds.map((c: string) => '`' + c + '$`').join(', ')
|
|
@@ -2714,8 +2343,9 @@ You should see:
|
|
|
2714
2343
|
\`\`\`
|
|
2715
2344
|
|
|
2716
2345
|
Two details of that configuration are worth a moment. The \`apikey\` is
|
|
2717
|
-
declared even though nothing here asks for credentials —
|
|
2718
|
-
|
|
2346
|
+
declared even though nothing here asks for credentials — ${provider.authActive ?
|
|
2347
|
+
'an empty\nvalue simply means no credential is sent' :
|
|
2348
|
+
'this API declares\nno authentication, so the value is never read'}. Every Seneca
|
|
2719
2349
|
provider is configured the same way, so an application that later moves
|
|
2720
2350
|
to an authenticated service changes one value rather than its shape.
|
|
2721
2351
|
And \`get:info\` is answered by the plugin itself, without calling the
|
|
@@ -2818,9 +2448,13 @@ so add:
|
|
|
2818
2448
|
console.log('created with id', ${subjOne}.${idf})
|
|
2819
2449
|
\`\`\`
|
|
2820
2450
|
|
|
2821
|
-
|
|
2451
|
+
${true === subject.rkoncreate ?
|
|
2452
|
+
`Run it, and note the id printed: it is the \`${subject.rk}\` you sent.
|
|
2453
|
+
The ${source} addresses ${subject.name} records by that key rather than by an
|
|
2454
|
+
id of its own, and the provider carries it as the entity's id.` :
|
|
2455
|
+
`Run it, and note the id printed. It is **not** one you chose — the
|
|
2822
2456
|
${source} assigns ids itself and ignores any you send. That is worth
|
|
2823
|
-
knowing before you write code that assumes otherwise
|
|
2457
|
+
knowing before you write code that assumes otherwise.`}
|
|
2824
2458
|
|
|
2825
2459
|
`)
|
|
2826
2460
|
|
|
@@ -2830,10 +2464,10 @@ rather than a create, and \`save$\` decides between the two on exactly
|
|
|
2830
2464
|
that:
|
|
2831
2465
|
|
|
2832
2466
|
\`\`\`js
|
|
2833
|
-
${subjOne}.${
|
|
2467
|
+
${subjOne}.${changeFields[0].name} = ${litval(changeFields[0], true)}
|
|
2834
2468
|
${subjOne} = await ${subjOne}.save$()
|
|
2835
2469
|
|
|
2836
|
-
console.log('updated:', ${subjOne}.${
|
|
2470
|
+
console.log('updated:', ${subjOne}.${changeFields[0].name})
|
|
2837
2471
|
\`\`\`
|
|
2838
2472
|
|
|
2839
2473
|
`)
|
|
@@ -2882,7 +2516,7 @@ They behave the same way on every entity this plugin exposes.
|
|
|
2882
2516
|
|
|
2883
2517
|
if (null != child) {
|
|
2884
2518
|
const ckey = child.parents[0]
|
|
2885
|
-
const cidf =
|
|
2519
|
+
const cidf = 'id'
|
|
2886
2520
|
const cparent = null == childParent ? 'their parent' :
|
|
2887
2521
|
`${childParent.name} records`
|
|
2888
2522
|
const cop = child.cmds.includes('list') ? 'list' : 'load'
|
|
@@ -2899,7 +2533,7 @@ They behave the same way on every entity this plugin exposes.
|
|
|
2899
2533
|
` const ${plural(childParent.name)} = await seneca
|
|
2900
2534
|
.entity('${canon(childParent.name)}')
|
|
2901
2535
|
.list$()
|
|
2902
|
-
const ${ident(ckey)} = ${plural(childParent.name)}[0]
|
|
2536
|
+
const ${ident(ckey)} = ${plural(childParent.name)}[0].id
|
|
2903
2537
|
|
|
2904
2538
|
` : ''
|
|
2905
2539
|
|
|
@@ -3004,7 +2638,9 @@ the way you saw:
|
|
|
3004
2638
|
}
|
|
3005
2639
|
if (canWrite) {
|
|
3006
2640
|
Content(`- \`save$\` creates without an id and updates with one, and the
|
|
3007
|
-
${
|
|
2641
|
+
${true === subject.rkoncreate ?
|
|
2642
|
+
`id is the \`${subject.rk}\` the create sends.` :
|
|
2643
|
+
`${source} chooses the id.`}
|
|
3008
2644
|
`)
|
|
3009
2645
|
}
|
|
3010
2646
|
if (offline) {
|
|
@@ -3030,21 +2666,6 @@ the way you saw:
|
|
|
3030
2666
|
})
|
|
3031
2667
|
|
|
3032
2668
|
|
|
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
2669
|
|
|
3049
2670
|
const DocHowto = cmp(function DocHowto(props: any) {
|
|
3050
2671
|
const { provider } = props
|
|
@@ -3072,19 +2693,20 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
3072
2693
|
}
|
|
3073
2694
|
|
|
3074
2695
|
const canon = (e: any) => `provider/${provider.lower}/${e.name}`
|
|
3075
|
-
|
|
2696
|
+
|
|
2697
|
+
// Seneca's key, on every entity: the provider translates it to whatever
|
|
2698
|
+
// the API calls it. `apiKey` is that name, for the SDK-direct examples.
|
|
2699
|
+
const idf = (_e: any) => 'id'
|
|
2700
|
+
const apiKey = (e: any) => e.rk || 'id'
|
|
3076
2701
|
|
|
3077
2702
|
// A parent key's example value. This MIRRORS seedRecord rather than
|
|
3078
2703
|
// inventing something more readable: the offline recipe below seeds with
|
|
3079
2704
|
// seedRecord, and an example id that does not match what was seeded turns
|
|
3080
2705
|
// every other recipe into a lookup that answers null.
|
|
3081
|
-
const parentVal = (e: any, k: string) =>
|
|
3082
|
-
const f = e.fields.find((f: any) => f.name === k)
|
|
3083
|
-
return null == f ? `${k.replace(/_id$/, '')}0` : `${f.parentEntity}0`
|
|
3084
|
-
}
|
|
2706
|
+
const parentVal = (e: any, k: string) => parentSeed(e, k)
|
|
3085
2707
|
|
|
3086
2708
|
const parentArgs = (e: any) =>
|
|
3087
|
-
e.parents.map((k: string) => `${k}: '${parentVal(e, k)}'`).join(', ')
|
|
2709
|
+
e.parents.map((k: string) => `${jsKey(k)}: '${parentVal(e, k)}'`).join(', ')
|
|
3088
2710
|
|
|
3089
2711
|
// A query naming ONE record. A top-level entity takes the bare id string;
|
|
3090
2712
|
// a nested one cannot, because it is identified by the whole set of keys.
|
|
@@ -3096,18 +2718,11 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
3096
2718
|
|
|
3097
2719
|
// The SDK's own entity ops always take an object, even for a bare id.
|
|
3098
2720
|
const sdkLoadArgs = (e: any) => 0 === e.parents.length ?
|
|
3099
|
-
`{ ${
|
|
3100
|
-
`{ ${parentArgs(e)}, ${
|
|
2721
|
+
`{ ${jsKey(apiKey(e))}: '${e.name}0' }` :
|
|
2722
|
+
`{ ${parentArgs(e)}, ${jsKey(apiKey(e))}: '${e.name}0' }`
|
|
3101
2723
|
|
|
3102
2724
|
const key = (k: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : `'${k}'`
|
|
3103
2725
|
|
|
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
2726
|
const jsval = (v: any): string => {
|
|
3112
2727
|
if (null === v || undefined === v) {
|
|
3113
2728
|
return 'null'
|
|
@@ -3131,16 +2746,18 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
3131
2746
|
|
|
3132
2747
|
// What a create sends: the seeded record without its id, because the id is
|
|
3133
2748
|
// the API's to assign. Parent keys stay — a nested write carries them in
|
|
3134
|
-
// the data rather than the query
|
|
2749
|
+
// the data rather than the query — and so does a key the create request
|
|
2750
|
+
// requires, which is the caller's to supply.
|
|
3135
2751
|
const createData = (e: any) => {
|
|
3136
2752
|
const rec = seedRecord(e, 0)
|
|
3137
|
-
|
|
2753
|
+
if (true !== e.rkoncreate) {
|
|
2754
|
+
delete rec[apiKey(e)]
|
|
2755
|
+
}
|
|
3138
2756
|
delete rec.id
|
|
3139
2757
|
return rec
|
|
3140
2758
|
}
|
|
3141
2759
|
|
|
3142
|
-
const changeable = (e: any) => e.fields.find((f: any) =>
|
|
3143
|
-
f.name !== idf(e) && 'id' !== f.name && !e.parents.includes(f.name))
|
|
2760
|
+
const changeable = (e: any) => e.fields.find((f: any) => changeField(e, f))
|
|
3144
2761
|
|
|
3145
2762
|
const newValue = (f: any) => 'number' === f.kind ? '999' :
|
|
3146
2763
|
'boolean' === f.kind ? 'true' : `'${f.name}-changed'`
|
|
@@ -3199,9 +2816,10 @@ const ${eLoad.name} = await seneca
|
|
|
3199
2816
|
.entity('${canon(eLoad)}')
|
|
3200
2817
|
.load$(${oneArgs(eLoad)})
|
|
3201
2818
|
\`\`\`
|
|
3202
|
-
${'id' ===
|
|
3203
|
-
The
|
|
3204
|
-
|
|
2819
|
+
${'id' === apiKey(eLoad) ? '' : `
|
|
2820
|
+
The API addresses \`${eLoad.name}\` records by \`${apiKey(eLoad)}\`; the
|
|
2821
|
+
provider carries that value as the entity's \`id\`, so the query is the
|
|
2822
|
+
same as for any other entity.
|
|
3205
2823
|
`}
|
|
3206
2824
|
A record that is not there comes back as \`null\`. It is not an error and
|
|
3207
2825
|
it does not throw, so test the value rather than wrapping the call:
|
|
@@ -3418,28 +3036,59 @@ companion test server listens, so local development usually needs no
|
|
|
3418
3036
|
\`base\` at all.`}`)
|
|
3419
3037
|
|
|
3420
3038
|
|
|
3421
|
-
|
|
3039
|
+
if (!provider.authActive) {
|
|
3040
|
+
sec('Send an API key', `The ${provider.api} definition declares no authentication, so this plugin
|
|
3041
|
+
reads no key and adds no credential to any request. The \`apikey\` entry in
|
|
3042
|
+
the provider configuration is the convention's shape, and stays empty:
|
|
3043
|
+
|
|
3044
|
+
\`\`\`js
|
|
3045
|
+
.use('provider', {
|
|
3046
|
+
provider: {
|
|
3047
|
+
${provider.lower}: {
|
|
3048
|
+
keys: {
|
|
3049
|
+
apikey: { value: '' },
|
|
3050
|
+
},
|
|
3051
|
+
},
|
|
3052
|
+
},
|
|
3053
|
+
})
|
|
3054
|
+
\`\`\`
|
|
3055
|
+
|
|
3056
|
+
To send a header the definition does not describe, supply it through
|
|
3057
|
+
\`sdk\`; it goes on every request as given:
|
|
3058
|
+
|
|
3059
|
+
\`\`\`js
|
|
3060
|
+
.use('${provider.pkgName}', {
|
|
3061
|
+
sdk: { headers: { 'x-api-key': process.env.${provider.ENV}_APIKEY } },
|
|
3062
|
+
})
|
|
3063
|
+
\`\`\``)
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
sec('Send an API key', `Credentials are not a plugin option: they come through the provider
|
|
3422
3067
|
convention, so that every provider in an application is configured the
|
|
3423
3068
|
same way. Declare the variable with \`env\` and set the key under this
|
|
3424
3069
|
provider's name:
|
|
3425
3070
|
|
|
3426
3071
|
\`\`\`js
|
|
3427
3072
|
.use('env', {
|
|
3428
|
-
var: { $${provider.ENV}_APIKEY: String
|
|
3073
|
+
var: { $${provider.ENV}_APIKEY: String${provider.authBasic ?
|
|
3074
|
+
`, $${provider.ENV}_SECRET: String` : ''} },
|
|
3429
3075
|
})
|
|
3430
3076
|
.use('provider', {
|
|
3431
3077
|
provider: {
|
|
3432
3078
|
${provider.lower}: {
|
|
3433
3079
|
keys: {
|
|
3434
|
-
apikey: { value: '$${provider.ENV}_APIKEY' }
|
|
3080
|
+
apikey: { value: '$${provider.ENV}_APIKEY' },${provider.authBasic ? `
|
|
3081
|
+
secret: { value: '$${provider.ENV}_SECRET' },` : ''}
|
|
3435
3082
|
},
|
|
3436
3083
|
},
|
|
3437
3084
|
},
|
|
3438
3085
|
})
|
|
3439
3086
|
\`\`\`
|
|
3440
3087
|
|
|
3441
|
-
Every request then carries
|
|
3442
|
-
|
|
3088
|
+
Every request then carries ${credentialWire(provider)}.${provider.authBasic ? `
|
|
3089
|
+
HTTP Basic needs the pair: with either \`apikey\` or \`secret\` missing, no
|
|
3090
|
+
credential is sent.` : ''} An absent
|
|
3091
|
+
or empty key sends no credential at all, so an API that needs none
|
|
3443
3092
|
is configured in exactly the same shape with an empty value — which is
|
|
3444
3093
|
why it is worth writing even when there is nothing to send. An
|
|
3445
3094
|
application that later moves to an authenticated service then changes one
|
|
@@ -3453,6 +3102,7 @@ For a different scheme, set the header yourself. Headers supplied through
|
|
|
3453
3102
|
sdk: { headers: { 'x-api-key': process.env.${provider.ENV}_APIKEY } },
|
|
3454
3103
|
})
|
|
3455
3104
|
\`\`\``)
|
|
3105
|
+
}
|
|
3456
3106
|
|
|
3457
3107
|
|
|
3458
3108
|
sec('Check which plugin and SDK are running', `One message, and the thing to reach for when a deployment is behaving
|
|
@@ -3481,7 +3131,7 @@ released separately and most surprises live in the gap between them.`)
|
|
|
3481
3131
|
const dpe = eList || subject
|
|
3482
3132
|
const dpath = dpe.path || provider.probePath || '/'
|
|
3483
3133
|
const dparams = pathParams(dpath)
|
|
3484
|
-
const dval = (k: string) => (k ===
|
|
3134
|
+
const dval = (k: string) => (k === apiKey(dpe) || 'id' === k) ?
|
|
3485
3135
|
`${dpe.name}0` : `${k.replace(/_id$/, '')}0`
|
|
3486
3136
|
|
|
3487
3137
|
sec('Reach the SDK directly', `The entity API covers the operations the API model declares. For
|
|
@@ -3711,25 +3361,6 @@ ${s.body}
|
|
|
3711
3361
|
})
|
|
3712
3362
|
|
|
3713
3363
|
|
|
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
3364
|
|
|
3734
3365
|
const DocReference = cmp(function DocReference(props: any) {
|
|
3735
3366
|
const { provider } = props
|
|
@@ -3740,8 +3371,6 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3740
3371
|
// has nothing to select here.
|
|
3741
3372
|
const acting = provider.entities.filter((e: any) => 0 < e.actionList.length)
|
|
3742
3373
|
|
|
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
3374
|
const subject = [...provider.entities]
|
|
3746
3375
|
.sort((a: any, b: any) =>
|
|
3747
3376
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
@@ -3760,8 +3389,6 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3760
3389
|
|
|
3761
3390
|
const live = '' !== provider.liveBase
|
|
3762
3391
|
|
|
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
3392
|
const quick = live && subject.cmds.includes('save') && subject.cmds.includes('remove')
|
|
3766
3393
|
|
|
3767
3394
|
const canon = (e: any) => `provider/${provider.lower}/${e.name}`
|
|
@@ -3777,12 +3404,14 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3777
3404
|
// A query literal for the docs: parent keys first, then whatever else the
|
|
3778
3405
|
// command needs.
|
|
3779
3406
|
const query = (e: any, extra: string[]) =>
|
|
3780
|
-
`{ ${[...e.parents, ...extra].map((k: string) => `${k}: '...'`).join(', ')} }`
|
|
3407
|
+
`{ ${[...e.parents, ...extra].map((k: string) => `${jsKey(k)}: '...'`).join(', ')} }`
|
|
3781
3408
|
|
|
3782
3409
|
// How a single record is addressed. The `load$('x')` short form only works
|
|
3783
3410
|
// when the id field is literally `id`.
|
|
3784
|
-
const oneArg = (e: any) => 0 < e.parents.length ? query(e, [
|
|
3785
|
-
|
|
3411
|
+
const oneArg = (e: any) => 0 < e.parents.length ? query(e, ['id']) : `'...'`
|
|
3412
|
+
|
|
3413
|
+
const apiKeyOf = (e: any) => 0 < idPartsOf(e).length ?
|
|
3414
|
+
idPartsOf(e).join(String(e.idsep || '/')) : (e.rk || 'id')
|
|
3786
3415
|
|
|
3787
3416
|
// The required-key phrasing, which has to read correctly for one key as
|
|
3788
3417
|
// well as several.
|
|
@@ -3949,11 +3578,11 @@ A canon carries only the commands its API operations support — an entity the
|
|
|
3949
3578
|
API offers no delete for has no \`remove$\` — so the tables below are the
|
|
3950
3579
|
whole of what each one answers.
|
|
3951
3580
|
|
|
3952
|
-
| Seneca canon | SDK accessor | Route |
|
|
3953
|
-
| ------------ | ------------ | ----- |
|
|
3581
|
+
| Seneca canon | SDK accessor | Route | API key | Parent keys | Commands |
|
|
3582
|
+
| ------------ | ------------ | ----- | ------- | ----------- | -------- |
|
|
3954
3583
|
`)
|
|
3955
3584
|
each(provider.entities, (e: any) => {
|
|
3956
|
-
Content(`| \`${canon(e)}\` | \`sdk.${e.acc}()\` | \`${e.path}\` | \`${e
|
|
3585
|
+
Content(`| \`${canon(e)}\` | \`sdk.${e.acc}()\` | \`${e.path}\` | \`${apiKeyOf(e)}\` | ${0 < e.parents.length ?
|
|
3957
3586
|
keys(e.parents) : '—'} | ${cmdList(e)} |
|
|
3958
3587
|
`)
|
|
3959
3588
|
})
|
|
@@ -3989,7 +3618,7 @@ before any request is made, rather than issuing one that would 404.
|
|
|
3989
3618
|
`)
|
|
3990
3619
|
}
|
|
3991
3620
|
if (e.cmds.includes('load')) {
|
|
3992
|
-
Content(`| \`load$(q)\` | ${reqd([...e.parents,
|
|
3621
|
+
Content(`| \`load$(q)\` | ${reqd([...e.parents, 'id'])} | One \`${e.name}\`, or \`null\` if not found. |
|
|
3993
3622
|
`)
|
|
3994
3623
|
}
|
|
3995
3624
|
if (e.cmds.includes('save')) {
|
|
@@ -4001,24 +3630,19 @@ before any request is made, rather than issuing one that would 404.
|
|
|
4001
3630
|
`)
|
|
4002
3631
|
}
|
|
4003
3632
|
if (e.cmds.includes('remove')) {
|
|
4004
|
-
Content(`| \`remove$(q)\` | ${reqd([...e.parents,
|
|
3633
|
+
Content(`| \`remove$(q)\` | ${reqd([...e.parents, 'id'])} | \`null\`. |
|
|
4005
3634
|
`)
|
|
4006
3635
|
}
|
|
4007
3636
|
|
|
4008
|
-
|
|
4009
|
-
// anything else never reads. Nested entities need the object form for
|
|
4010
|
-
// their parent keys anyway, so this only needs saying for top-level ones.
|
|
4011
|
-
const shortForm = 0 === e.parents.length && 'id' !== e.idf ?
|
|
4012
|
-
e.cmds.filter((c: string) => 'load' === c || 'remove' === c) : []
|
|
4013
|
-
|
|
4014
|
-
if (0 < shortForm.length) {
|
|
3637
|
+
if ('id' !== apiKeyOf(e)) {
|
|
4015
3638
|
Content(`
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
3639
|
+
The API addresses \`${e.name}\` records by \`${apiKeyOf(e)}\`; the provider
|
|
3640
|
+
carries that value as the entity's \`id\`, so every query and entity above
|
|
3641
|
+
uses \`id\`. A record the API returns with an unrelated \`id\` of its own
|
|
3642
|
+
${false === e.parkfree ?
|
|
3643
|
+
`keeps it where it is: \`${provider.lower}_id\`, where this provider
|
|
3644
|
+
would otherwise park it, is a name \`${e.name}\` itself uses.` :
|
|
3645
|
+
`keeps that under \`${provider.lower}_id\`.`}
|
|
4022
3646
|
`)
|
|
4023
3647
|
}
|
|
4024
3648
|
|
|
@@ -4037,7 +3661,8 @@ also defines are passed through unchanged in both directions.
|
|
|
4037
3661
|
| ----- | ---- | ----- |
|
|
4038
3662
|
`)
|
|
4039
3663
|
each(e.fields, (f: any) => {
|
|
4040
|
-
Content(`| \`${f.name}\` | ${f.kind} | ${f.name === e.
|
|
3664
|
+
Content(`| \`${f.name}\` | ${f.kind} | ${f.name === (e.rk || 'id') ?
|
|
3665
|
+
('id' === f.name ? 'Id field.' : 'API key; carried as the entity\'s `id`.') :
|
|
4041
3666
|
e.parents.includes(f.name) ? ('' === f.parentEntity ?
|
|
4042
3667
|
'Parent key. Required by every command.' :
|
|
4043
3668
|
`Parent key: the id of a \`${f.parentEntity}\`. Required by every command.`) : ''} |
|
|
@@ -4070,15 +3695,14 @@ also defines are passed through unchanged in both directions.
|
|
|
4070
3695
|
// The dispatching entity to show it with: the subject when it qualifies,
|
|
4071
3696
|
// otherwise the first that does.
|
|
4072
3697
|
const s = dispatch.includes(subject) ? subject : dispatch[0]
|
|
4073
|
-
const writable = s.fields
|
|
4074
|
-
|
|
4075
|
-
.filter((f: any) => !s.parents.includes(f.name))
|
|
3698
|
+
const writable = s.fields.filter((f: any) => ownField(s, f))
|
|
3699
|
+
const alterable = writable.filter((f: any) => changeField(s, f))
|
|
4076
3700
|
const value = (f: any, alt: boolean) => 'number' === f.kind ?
|
|
4077
3701
|
(alt ? '4321' : '1234') : 'boolean' === f.kind ?
|
|
4078
3702
|
(alt ? 'true' : 'false') : `'${f.name}${alt ? '-changed' : '-value'}'`
|
|
4079
3703
|
const make = [
|
|
4080
|
-
...s.parents.map((k: string) => `${k}: '...'`),
|
|
4081
|
-
...writable.map((f: any) => `${f.name}: ${value(f, false)}`),
|
|
3704
|
+
...s.parents.map((k: string) => `${jsKey(k)}: '...'`),
|
|
3705
|
+
...writable.map((f: any) => `${jsKey(f.name)}: ${value(f, false)}`),
|
|
4082
3706
|
].join(', ')
|
|
4083
3707
|
|
|
4084
3708
|
Content(`
|
|
@@ -4089,14 +3713,14 @@ created, an entity **with** one is updated. The provider dispatches on the
|
|
|
4089
3713
|
id field, so the same call does both.
|
|
4090
3714
|
|
|
4091
3715
|
\`\`\`js
|
|
4092
|
-
// Create — no
|
|
3716
|
+
// Create — no id.
|
|
4093
3717
|
const ${s.name} = await seneca
|
|
4094
3718
|
.entity('${canon(s)}')
|
|
4095
3719
|
.make$({ ${make} })
|
|
4096
3720
|
.save$()
|
|
4097
3721
|
|
|
4098
|
-
// Update —
|
|
4099
|
-
${0 <
|
|
3722
|
+
// Update — id present.
|
|
3723
|
+
${0 < alterable.length ? `${s.name}.${alterable[0].name} = ${value(alterable[0], true)}
|
|
4100
3724
|
` : ''}await ${s.name}.save$()
|
|
4101
3725
|
\`\`\`
|
|
4102
3726
|
|
|
@@ -4348,19 +3972,26 @@ it is absent, \`null\` or the empty string.
|
|
|
4348
3972
|
|
|
4349
3973
|
Content(`
|
|
4350
3974
|
## Authentication keys
|
|
4351
|
-
|
|
3975
|
+
${!provider.authActive ? `
|
|
3976
|
+
The ${provider.api} definition declares no authentication. The plugin reads
|
|
3977
|
+
no key and adds no credential to any request: \`sys:provider,get:keymap\` is
|
|
3978
|
+
never posted. An \`apikey\` configured under this provider's name is
|
|
3979
|
+
accepted, for uniformity with other providers, and ignored.
|
|
3980
|
+
` : `
|
|
4352
3981
|
The plugin follows the provider convention: if an \`apikey\` key is
|
|
4353
|
-
configured and non-empty, it is sent as
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
no credential
|
|
3982
|
+
configured and non-empty, it is sent as ${credentialWire(provider)} on every
|
|
3983
|
+
request.${provider.authBasic ? ` HTTP Basic needs a second key, \`secret\`; with
|
|
3984
|
+
either missing, no credential is sent.` : ''} If the provider is not
|
|
3985
|
+
registered, or the key is absent or empty, no credential is added and
|
|
3986
|
+
startup proceeds with a warning in the log.
|
|
4357
3987
|
|
|
4358
3988
|
\`\`\`js
|
|
4359
3989
|
.use('provider', {
|
|
4360
3990
|
provider: {
|
|
4361
3991
|
${provider.lower}: {
|
|
4362
3992
|
keys: {
|
|
4363
|
-
apikey: { value: '$${provider.ENV}_APIKEY' }
|
|
3993
|
+
apikey: { value: '$${provider.ENV}_APIKEY' },${provider.authBasic ? `
|
|
3994
|
+
secret: { value: '$${provider.ENV}_SECRET' },` : ''}
|
|
4364
3995
|
},
|
|
4365
3996
|
},
|
|
4366
3997
|
},
|
|
@@ -4368,9 +3999,10 @@ no credential exercises the same path.
|
|
|
4368
3999
|
\`\`\`
|
|
4369
4000
|
|
|
4370
4001
|
The key is read once, during \`seneca.prepare()\`, by posting
|
|
4371
|
-
\`sys:provider,get:keymap,provider:${provider.lower}\`.
|
|
4372
|
-
|
|
4373
|
-
|
|
4002
|
+
\`sys:provider,get:keymap,provider:${provider.lower}\`. A header supplied
|
|
4003
|
+
through the \`sdk.headers\` option takes precedence over the one the key
|
|
4004
|
+
would set.
|
|
4005
|
+
`}
|
|
4374
4006
|
## Environment variables
|
|
4375
4007
|
|
|
4376
4008
|
The plugin never reads the environment itself. These are the variables the
|
|
@@ -4450,28 +4082,10 @@ ${quick ? 'Both scripts target' : 'It targets'} \`$${provider.ENV}_TEST_BASE\`,
|
|
|
4450
4082
|
})
|
|
4451
4083
|
|
|
4452
4084
|
|
|
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
4085
|
|
|
4469
4086
|
const DocExplanation = cmp(function DocExplanation(props: any) {
|
|
4470
4087
|
const { provider } = props
|
|
4471
4088
|
|
|
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
4089
|
const subject = [...provider.entities]
|
|
4476
4090
|
.sort((a: any, b: any) =>
|
|
4477
4091
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
@@ -4777,12 +4391,19 @@ by hand. Nothing about the mapping is waiting to be written.
|
|
|
4777
4391
|
`)
|
|
4778
4392
|
}
|
|
4779
4393
|
|
|
4780
|
-
Content(`## Credentials, whether or not the API needs them
|
|
4394
|
+
Content(provider.authActive ? `## Credentials, whether or not the API needs them
|
|
4781
4395
|
|
|
4782
4396
|
At startup the plugin asks \`@seneca/provider\` for the keymap of
|
|
4783
|
-
\`${provider.lower}\` and sends the \`apikey\` as
|
|
4784
|
-
configured.
|
|
4397
|
+
\`${provider.lower}\` and sends the \`apikey\` as ${credentialWire(provider)}
|
|
4398
|
+
when one is configured.
|
|
4399
|
+
` : `## Credentials, for an API that declares none
|
|
4785
4400
|
|
|
4401
|
+
The ${provider.api} definition declares no authentication, so the plugin
|
|
4402
|
+
plumbs no credential: it does not ask \`@seneca/provider\` for a keymap at
|
|
4403
|
+
startup, and adds nothing to a request. The SDK's own auth stage is empty
|
|
4404
|
+
for such a definition, so a key handed to it could not reach the wire.
|
|
4405
|
+
`)
|
|
4406
|
+
Content(`
|
|
4786
4407
|
The key is *optional*. Absent, unconfigured and empty all mean "send no
|
|
4787
4408
|
header", and none of them is an error. For an API that needs no credential this
|
|
4788
4409
|
looks like ceremony, and it is worth keeping anyway: the shape of a Seneca
|
|
@@ -4919,23 +4540,6 @@ order-dependent and then flaky.
|
|
|
4919
4540
|
})
|
|
4920
4541
|
|
|
4921
4542
|
|
|
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
4543
|
|
|
4940
4544
|
const DocIndex = cmp(function DocIndex(props: any) {
|
|
4941
4545
|
const { provider } = props
|
|
@@ -4987,9 +4591,6 @@ components, where fixing it once fixes every provider.
|
|
|
4987
4591
|
})
|
|
4988
4592
|
|
|
4989
4593
|
|
|
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
4594
|
const Docs = cmp(function Docs(props: any) {
|
|
4994
4595
|
const { provider } = props
|
|
4995
4596
|
|