@voxgig/sdkgen-infrapack 0.0.13 → 0.0.15
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 → seneca-provider.aontu} +6 -4
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +253 -142
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +168 -78
- package/README.md +8 -2
- package/package.json +35 -15
- package/sdkgen-package.json +2 -2
|
@@ -80,10 +80,68 @@ function parentSeed(e: any, key: string): string {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
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
|
+
|
|
83
105
|
function parentPairs(e: any, live: boolean): string {
|
|
84
|
-
return e.parents
|
|
85
|
-
|
|
86
|
-
|
|
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}`
|
|
87
145
|
}
|
|
88
146
|
|
|
89
147
|
|
|
@@ -161,9 +219,7 @@ function queryPairs(e: any, live: boolean): string {
|
|
|
161
219
|
|
|
162
220
|
const rest = e.parents.filter((p: string) => !parts.includes(p))
|
|
163
221
|
|
|
164
|
-
return rest
|
|
165
|
-
.map((p: string) => live ? `${p}, ` : `${p}: '${parentSeed(e, p)}', `)
|
|
166
|
-
.join('')
|
|
222
|
+
return rest.map((p: string) => parentPair(e, p, live)).join('')
|
|
167
223
|
}
|
|
168
224
|
|
|
169
225
|
|
|
@@ -228,7 +284,7 @@ ${ind} .list\$()
|
|
|
228
284
|
|
|
229
285
|
${ind} if (0 === ${pv}.length) return t.skip('no ${pe.name} to attach a ${e.name} to')
|
|
230
286
|
|
|
231
|
-
${ind} const ${p} = ${pv}[0]
|
|
287
|
+
${ind} const ${paramVar(p)} = ${pv}[0].id
|
|
232
288
|
|
|
233
289
|
`
|
|
234
290
|
}).join('')
|
|
@@ -241,8 +297,7 @@ ${ind} const ${p} = ${pv}[0].${pe.idf || 'id'}
|
|
|
241
297
|
// is dropped rather than asserted vacuously.
|
|
242
298
|
function mutableField(e: any): string {
|
|
243
299
|
const f = (e.fields || []).find((f: any) =>
|
|
244
|
-
f
|
|
245
|
-
!e.parents.includes(f.name) && 'string' === f.kind)
|
|
300
|
+
changeField(e, f) && 'string' === f.kind)
|
|
246
301
|
|
|
247
302
|
return f ? f.name : ''
|
|
248
303
|
}
|
|
@@ -258,14 +313,16 @@ function crudTest(provider: any, e: any, mode: 'offline' | 'live'): string {
|
|
|
258
313
|
// query and entity always spell the id `id`. The provider translates to
|
|
259
314
|
// whatever the API calls it.
|
|
260
315
|
const idf = 'id'
|
|
261
|
-
|
|
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) : ''
|
|
262
320
|
|
|
263
321
|
const ind = live ? ' ' : ' '
|
|
264
322
|
const mk = live ? 'makeSeneca(liveOpts())' : 'makeSeneca()'
|
|
265
323
|
const setup = live ? liveParentSetup(provider, e, ind) : ''
|
|
266
324
|
|
|
267
|
-
const made = 0 < e.fields.filter((f: any) =>
|
|
268
|
-
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 ?
|
|
269
326
|
seedLiteral(e, 'crud') : ''
|
|
270
327
|
|
|
271
328
|
const idmake = 0 === idPartsOf(e).length ? '' :
|
|
@@ -320,8 +377,11 @@ ${ind} }
|
|
|
320
377
|
${live ? `${ind} if (!live) return t.skip(noServer())\n` : ''}${ind} const seneca = await ${mk}
|
|
321
378
|
${ind} const ent = seneca.entity('provider/${provider.lower}/${e.name}')
|
|
322
379
|
|
|
323
|
-
${setup}${ind} // Seneca's convention: an entity WITHOUT an id is a create.
|
|
324
|
-
|
|
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.`}
|
|
325
385
|
${ind} const made = await ent.make$({ ${pairs}${made}${idmake} }).save$()
|
|
326
386
|
|
|
327
387
|
${ind} assert.ok(null != made.${idf})
|
|
@@ -354,8 +414,7 @@ function fieldLiteral(f: any, tag: string): string {
|
|
|
354
414
|
// shares with the seed.
|
|
355
415
|
function seedLiteral(e: any, tag: string): string {
|
|
356
416
|
return (e.fields || [])
|
|
357
|
-
.filter((f: any) =>
|
|
358
|
-
f.name !== e.idf && 'id' !== f.name && !e.parents.includes(f.name))
|
|
417
|
+
.filter((f: any) => ownField(e, f))
|
|
359
418
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, tag)}`)
|
|
360
419
|
.join(', ')
|
|
361
420
|
}
|
|
@@ -612,7 +671,7 @@ describe('${provider.fileBase}', () => {
|
|
|
612
671
|
.entity('provider/${provider.lower}/${e.name}')
|
|
613
672
|
.load$('${entIdLiteral(e, '0')}')
|
|
614
673
|
|
|
615
|
-
assert.equal(found
|
|
674
|
+
assert.equal(found.id, '${entIdLiteral(e, '0')}')
|
|
616
675
|
assert.equal(
|
|
617
676
|
found.canon$({ string: true }),
|
|
618
677
|
'provider/${provider.lower}/${e.name}',
|
|
@@ -653,7 +712,7 @@ describe('${provider.fileBase}', () => {
|
|
|
653
712
|
// syntax error ("Invalid regular expression flags"). Escape every
|
|
654
713
|
// regex metacharacter, not just the slash, so a future separator
|
|
655
714
|
// cannot reintroduce this.
|
|
656
|
-
const shapeRe = shape
|
|
715
|
+
const shapeRe = regexLiteral(shape)
|
|
657
716
|
const cmd = ['load', 'remove', 'update'].find((op: string) =>
|
|
658
717
|
e.cmds.includes('remove' === op ? 'remove' : 'load' === op ? 'load' : 'save'))
|
|
659
718
|
|
|
@@ -682,7 +741,7 @@ describe('${provider.fileBase}', () => {
|
|
|
682
741
|
|
|
683
742
|
const key = e.parents[0]
|
|
684
743
|
const pairs = e.parents
|
|
685
|
-
.map((k: string) => `${k}: '${parentSeed(e, k)}'`).join(', ')
|
|
744
|
+
.map((k: string) => `${jsKey(k)}: '${parentSeed(e, k)}'`).join(', ')
|
|
686
745
|
|
|
687
746
|
const guardOp = ['list', 'load', 'update', 'remove']
|
|
688
747
|
.find((op: string) => (e.opParents[op] || []).includes(key) &&
|
|
@@ -704,7 +763,7 @@ describe('${provider.fileBase}', () => {
|
|
|
704
763
|
|
|
705
764
|
await assert.rejects(
|
|
706
765
|
() => seneca.entity('provider/${provider.lower}/${e.name}').${call},
|
|
707
|
-
/${key} is required/,
|
|
766
|
+
/${regexLiteral(key)} is required/,
|
|
708
767
|
)
|
|
709
768
|
})
|
|
710
769
|
|
|
@@ -725,7 +784,7 @@ describe('${provider.fileBase}', () => {
|
|
|
725
784
|
)
|
|
726
785
|
${0 < idPartsOf(e).length ?
|
|
727
786
|
`assert.equal(list[0].id, '${entIdLiteral(e, '0')}')` :
|
|
728
|
-
`assert.equal(list[0]
|
|
787
|
+
`assert.equal(${jsProp('list[0]', key)}, '${parentSeed(e, key)}')`}
|
|
729
788
|
})
|
|
730
789
|
|
|
731
790
|
`)
|
|
@@ -770,7 +829,16 @@ ${!loadHasKey(e) ? '' : `
|
|
|
770
829
|
// transport implements create/update/remove, so this needs no server.
|
|
771
830
|
each(provider.entities, (e: any) => {
|
|
772
831
|
if (e.cmds.includes('save') && e.cmds.includes('remove')) {
|
|
773
|
-
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) &&
|
|
774
842
|
!cmdRefuses(e, 'update')) {
|
|
775
843
|
Content(`
|
|
776
844
|
` + crudTest(provider, e, 'offline'))
|
|
@@ -867,7 +935,7 @@ ${!loadHasKey(e) ? '' : `
|
|
|
867
935
|
|
|
868
936
|
await assert.rejects(
|
|
869
937
|
() => seneca.entity('provider/${provider.lower}/${e.name}')
|
|
870
|
-
.make$({ ${pairs}id: '${e
|
|
938
|
+
.make$({ ${pairs}id: '${entIdLiteral(e, '0')}' })
|
|
871
939
|
.directive$({ action$: 'no_such_action' })
|
|
872
940
|
.save$(),
|
|
873
941
|
/action\\$ "no_such_action" is not an action/,
|
|
@@ -929,7 +997,7 @@ ${!loadHasKey(e) ? '' : `
|
|
|
929
997
|
|
|
930
998
|
try {
|
|
931
999
|
await seneca.entity('provider/${provider.lower}/${e.name}')
|
|
932
|
-
.make$({ ${pairs}id: '${e
|
|
1000
|
+
.make$({ ${pairs}id: '${entIdLiteral(e, '0')}' })
|
|
933
1001
|
.directive$({ action$: '${act.action}' })
|
|
934
1002
|
.save$()
|
|
935
1003
|
}
|
|
@@ -977,17 +1045,25 @@ ${!loadHasKey(e) ? '' : `
|
|
|
977
1045
|
|
|
978
1046
|
`)
|
|
979
1047
|
}
|
|
980
|
-
|
|
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}' }`
|
|
981
1057
|
Content(` // A read of something that is not there is \`null\`, live as well as
|
|
982
1058
|
// offline: the provider's 404 handling is the same code path either way.
|
|
983
1059
|
it('${subject.name}-load-missing', async (t) => {
|
|
984
1060
|
if (!live) return t.skip(noServer())
|
|
985
1061
|
const seneca = await makeSeneca(liveOpts())
|
|
986
1062
|
|
|
987
|
-
assert.equal(
|
|
1063
|
+
${liveParentSetup(provider, subject, ' ')} assert.equal(
|
|
988
1064
|
await seneca
|
|
989
1065
|
.entity('provider/${provider.lower}/${subject.name}')
|
|
990
|
-
.load$(
|
|
1066
|
+
.load$(${missing}),
|
|
991
1067
|
null,
|
|
992
1068
|
)
|
|
993
1069
|
})
|
|
@@ -997,6 +1073,7 @@ ${!loadHasKey(e) ? '' : `
|
|
|
997
1073
|
|
|
998
1074
|
each(provider.entities, (e: any) => {
|
|
999
1075
|
if (e.cmds.includes('save') && e.cmds.includes('remove') &&
|
|
1076
|
+
e.canonicalOps.includes('create') &&
|
|
1000
1077
|
liveParentsResolvable(provider, e) && compositeRoundTrip(e)) {
|
|
1001
1078
|
Content(crudTest(provider, e, 'live'))
|
|
1002
1079
|
}
|
|
@@ -1177,7 +1254,7 @@ async function run() {
|
|
|
1177
1254
|
Content(` // ${e.name}: needs ${e.parents.join(', ')}; no listable parent to take
|
|
1178
1255
|
// one from, so supply it yourself:
|
|
1179
1256
|
// await seneca.entity('provider/${provider.lower}/${e.name}')
|
|
1180
|
-
// .list$({ ${e.parents.map((k: string) => `${k}: '...'`).join(', ')} })
|
|
1257
|
+
// .list$({ ${e.parents.map((k: string) => `${jsKey(k)}: '...'`).join(', ')} })
|
|
1181
1258
|
|
|
1182
1259
|
`)
|
|
1183
1260
|
return
|
|
@@ -1191,7 +1268,7 @@ async function run() {
|
|
|
1191
1268
|
if (0 < ${parent.name}s.length) {
|
|
1192
1269
|
console.log('${e.name.toUpperCase()}', await seneca
|
|
1193
1270
|
.entity('provider/${provider.lower}/${e.name}')
|
|
1194
|
-
.list$({ ${key}: ${parent.name}s[0]
|
|
1271
|
+
.list$({ ${jsKey(key)}: ${parent.name}s[0].id }))
|
|
1195
1272
|
}
|
|
1196
1273
|
|
|
1197
1274
|
`)
|
|
@@ -1204,10 +1281,8 @@ async function run() {
|
|
|
1204
1281
|
// The write cycle, kept separate: it MUTATES the server, so it is not
|
|
1205
1282
|
// something to run by reflex. It cleans up after itself.
|
|
1206
1283
|
if (subject.cmds.includes('save') && subject.cmds.includes('remove')) {
|
|
1207
|
-
const idf =
|
|
1208
|
-
const writable = subject.fields
|
|
1209
|
-
.filter((f: any) => f.name !== idf && f.name !== 'id')
|
|
1210
|
-
.filter((f: any) => !subject.parents.includes(f.name))
|
|
1284
|
+
const idf = 'id'
|
|
1285
|
+
const writable = subject.fields.filter((f: any) => ownField(subject, f))
|
|
1211
1286
|
|
|
1212
1287
|
const make = writable
|
|
1213
1288
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, 'quick')}`)
|
|
@@ -1231,7 +1306,9 @@ run()
|
|
|
1231
1306
|
async function run() {
|
|
1232
1307
|
const seneca = await makeSeneca()
|
|
1233
1308
|
|
|
1234
|
-
// 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.'}
|
|
1235
1312
|
let ${subject.name} = await seneca
|
|
1236
1313
|
.entity('provider/${provider.lower}/${subject.name}')
|
|
1237
1314
|
.make$({ ${make} })
|
|
@@ -1244,8 +1321,8 @@ async function run() {
|
|
|
1244
1321
|
`)
|
|
1245
1322
|
// Change something an assertion could SEE. A container field would be
|
|
1246
1323
|
// rewritten to the same empty literal, which demonstrates nothing.
|
|
1247
|
-
const upd = writable.find((f: any) =>
|
|
1248
|
-
'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
|
|
1249
1326
|
|
|
1250
1327
|
if (subject.ops.includes('update') && null != upd) {
|
|
1251
1328
|
const f = upd
|
|
@@ -1272,10 +1349,9 @@ async function run() {
|
|
|
1272
1349
|
|
|
1273
1350
|
if (null != child) {
|
|
1274
1351
|
const ckey = child.parents[0]
|
|
1275
|
-
const cidf =
|
|
1352
|
+
const cidf = 'id'
|
|
1276
1353
|
const cmake = (child.fields || [])
|
|
1277
|
-
.filter((f: any) =>
|
|
1278
|
-
f.name !== cidf && 'id' !== f.name && !child.parents.includes(f.name))
|
|
1354
|
+
.filter((f: any) => ownField(child, f))
|
|
1279
1355
|
.map((f: any) => `${jsKey(f.name)}: ${fieldLiteral(f, 'quick')}`)
|
|
1280
1356
|
.join(', ')
|
|
1281
1357
|
|
|
@@ -1283,13 +1359,13 @@ async function run() {
|
|
|
1283
1359
|
// goes under the ${subject.name} just created — and comes back off again.
|
|
1284
1360
|
const ${child.name} = await seneca
|
|
1285
1361
|
.entity('provider/${provider.lower}/${child.name}')
|
|
1286
|
-
.make$({ ${ckey}: id${'' === cmake ? '' : ', ' + cmake} })
|
|
1362
|
+
.make$({ ${jsKey(ckey)}: id${'' === cmake ? '' : ', ' + cmake} })
|
|
1287
1363
|
.save$()
|
|
1288
1364
|
console.log('${child.name.toUpperCase()} CREATED', ${child.name})
|
|
1289
1365
|
|
|
1290
1366
|
await seneca
|
|
1291
1367
|
.entity('provider/${provider.lower}/${child.name}')
|
|
1292
|
-
.remove$({ ${ckey}: id, ${cidf}: ${child.name}.${cidf} })
|
|
1368
|
+
.remove$({ ${jsKey(ckey)}: id, ${cidf}: ${child.name}.${cidf} })
|
|
1293
1369
|
console.log('${child.name.toUpperCase()} REMOVED')
|
|
1294
1370
|
|
|
1295
1371
|
`)
|
|
@@ -1418,12 +1494,6 @@ ${!provider.liveApp ? '' : `
|
|
|
1418
1494
|
`}
|
|
1419
1495
|
- run: npm install${provider.sdkInstallFlag}
|
|
1420
1496
|
|
|
1421
|
-
# The Seneca host framework is a PEER dependency, so the test suite needs
|
|
1422
|
-
# it installed explicitly. --no-save keeps npm from rewriting the peer
|
|
1423
|
-
# ranges in package.json to carets on what it happened to resolve, which
|
|
1424
|
-
# would have the build testing a manifest the repo never authored.
|
|
1425
|
-
- run: npm i --no-save seneca seneca-entity seneca-promisify @seneca/provider @seneca/env
|
|
1426
|
-
|
|
1427
1497
|
- run: npm run build --if-present
|
|
1428
1498
|
- run: npm test
|
|
1429
1499
|
`)
|
|
@@ -1496,16 +1566,6 @@ jobs:
|
|
|
1496
1566
|
# install, not ci: this package does not commit a lockfile.
|
|
1497
1567
|
- run: npm install${provider.sdkInstallFlag}
|
|
1498
1568
|
|
|
1499
|
-
# The Seneca host framework is a PEER dependency, so the test suite
|
|
1500
|
-
# needs it installed explicitly.
|
|
1501
|
-
#
|
|
1502
|
-
# --no-save IS LOAD-BEARING. Without it npm rewrites the peer ranges in
|
|
1503
|
-
# package.json to carets on whatever it resolved, and a later publish
|
|
1504
|
-
# ships that rewritten manifest — so an authored \`>=26\` reaches
|
|
1505
|
-
# consumers as \`^28.1.0\` and the package refuses to install for anyone
|
|
1506
|
-
# on a newer major. The repo looks fine; only the artifact is narrowed.
|
|
1507
|
-
- run: npm i --no-save seneca seneca-entity seneca-promisify @seneca/provider @seneca/env
|
|
1508
|
-
|
|
1509
1569
|
- run: npm run build
|
|
1510
1570
|
- run: npm test
|
|
1511
1571
|
|
|
@@ -1675,7 +1735,7 @@ await seneca.ready()
|
|
|
1675
1735
|
null != subject.idsep && '' !== String(subject.idsep) ?
|
|
1676
1736
|
String(subject.idsep) : '/')}'` :
|
|
1677
1737
|
0 === subject.parents.length ? `'some-id'` :
|
|
1678
|
-
`{ ` + subject.parents.map((p: string) => `${p}: 'some-${p}'`).join(', ') +
|
|
1738
|
+
`{ ` + subject.parents.map((p: string) => `${jsKey(p)}: 'some-${p}'`).join(', ') +
|
|
1679
1739
|
`, id: 'some-id' }`
|
|
1680
1740
|
Content(`const ${subject.name} = await seneca
|
|
1681
1741
|
.entity('provider/${provider.lower}/${subject.name}').load$(${loadArg})
|
|
@@ -1959,7 +2019,7 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
1959
2019
|
.sort((a: any, b: any) =>
|
|
1960
2020
|
(a.parents.length - b.parents.length) || (b.cmds.length - a.cmds.length))[0]
|
|
1961
2021
|
|
|
1962
|
-
const idf =
|
|
2022
|
+
const idf = 'id'
|
|
1963
2023
|
const subjParent = 0 < subject.parents.length ?
|
|
1964
2024
|
entOf(subject.parentEntity) : null
|
|
1965
2025
|
|
|
@@ -1993,19 +2053,14 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
1993
2053
|
|
|
1994
2054
|
// The value seedRecord() gives a parent key, so a query written here finds
|
|
1995
2055
|
// the seeded record instead of quietly matching nothing.
|
|
1996
|
-
const seedParentVal = (e: any, k: string) =>
|
|
1997
|
-
const f = (e.fields || []).find((f: any) => f.name === k)
|
|
1998
|
-
const pe = (null != f && '' !== f.parentEntity) ? f.parentEntity :
|
|
1999
|
-
(k === e.parents[0] ? (e.parentEntity || '') : '')
|
|
2000
|
-
return `${pe}0`
|
|
2001
|
-
}
|
|
2056
|
+
const seedParentVal = (e: any, k: string) => parentSeed(e, k)
|
|
2002
2057
|
|
|
2003
2058
|
// A seed record guaranteed to carry its id and its parent keys.
|
|
2004
2059
|
// seedRecord() emits only the fields the model marks required, and a record
|
|
2005
2060
|
// missing its parent key is invisible to the very query this lesson makes.
|
|
2006
2061
|
const demoRecord = (e: any, idx: number) => {
|
|
2007
2062
|
const rec: any = seedRecord(e, idx)
|
|
2008
|
-
const eidf = e.
|
|
2063
|
+
const eidf = e.rk || 'id'
|
|
2009
2064
|
if (null == rec[eidf]) {
|
|
2010
2065
|
rec[eidf] = `${e.name}${idx}`
|
|
2011
2066
|
}
|
|
@@ -2055,13 +2110,13 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2055
2110
|
const ${plural(subjParent.name)} = await seneca
|
|
2056
2111
|
.entity('${canon(subjParent.name)}')
|
|
2057
2112
|
.list$()
|
|
2058
|
-
const ${ident(subject.parents[0])} = ${plural(subjParent.name)}[0]
|
|
2113
|
+
const ${ident(subject.parents[0])} = ${plural(subjParent.name)}[0].id
|
|
2059
2114
|
|
|
2060
2115
|
` : ''
|
|
2061
2116
|
|
|
2062
2117
|
// Fields worth printing, and worth writing: not the id, not a parent key.
|
|
2063
|
-
const plainFields = subject.fields.filter((f: any) =>
|
|
2064
|
-
|
|
2118
|
+
const plainFields = subject.fields.filter((f: any) => ownField(subject, f))
|
|
2119
|
+
const changeFields = plainFields.filter((f: any) => changeField(subject, f))
|
|
2065
2120
|
const shown = plainFields.slice(0, 2)
|
|
2066
2121
|
const litval = (f: any, alt: boolean) =>
|
|
2067
2122
|
'number' === f.kind ? (alt ? '4321' : '1234') :
|
|
@@ -2077,7 +2132,8 @@ const DocTutorial = cmp(function DocTutorial(props: any) {
|
|
|
2077
2132
|
// Creating a record with nothing in it teaches nothing, so the write step
|
|
2078
2133
|
// needs at least one field the caller actually supplies.
|
|
2079
2134
|
const canWrite = subject.cmds.includes('save') && 0 < plainFields.length
|
|
2080
|
-
const canUpdate = canWrite && subject.ops.includes('update')
|
|
2135
|
+
const canUpdate = canWrite && subject.ops.includes('update') &&
|
|
2136
|
+
0 < changeFields.length
|
|
2081
2137
|
const canRemove = canWrite && subject.cmds.includes('remove')
|
|
2082
2138
|
|
|
2083
2139
|
const cmdList = subject.cmds.map((c: string) => '`' + c + '$`').join(', ')
|
|
@@ -2287,8 +2343,9 @@ You should see:
|
|
|
2287
2343
|
\`\`\`
|
|
2288
2344
|
|
|
2289
2345
|
Two details of that configuration are worth a moment. The \`apikey\` is
|
|
2290
|
-
declared even though nothing here asks for credentials —
|
|
2291
|
-
|
|
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
|
|
2292
2349
|
provider is configured the same way, so an application that later moves
|
|
2293
2350
|
to an authenticated service changes one value rather than its shape.
|
|
2294
2351
|
And \`get:info\` is answered by the plugin itself, without calling the
|
|
@@ -2391,9 +2448,13 @@ so add:
|
|
|
2391
2448
|
console.log('created with id', ${subjOne}.${idf})
|
|
2392
2449
|
\`\`\`
|
|
2393
2450
|
|
|
2394
|
-
|
|
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
|
|
2395
2456
|
${source} assigns ids itself and ignores any you send. That is worth
|
|
2396
|
-
knowing before you write code that assumes otherwise
|
|
2457
|
+
knowing before you write code that assumes otherwise.`}
|
|
2397
2458
|
|
|
2398
2459
|
`)
|
|
2399
2460
|
|
|
@@ -2403,10 +2464,10 @@ rather than a create, and \`save$\` decides between the two on exactly
|
|
|
2403
2464
|
that:
|
|
2404
2465
|
|
|
2405
2466
|
\`\`\`js
|
|
2406
|
-
${subjOne}.${
|
|
2467
|
+
${subjOne}.${changeFields[0].name} = ${litval(changeFields[0], true)}
|
|
2407
2468
|
${subjOne} = await ${subjOne}.save$()
|
|
2408
2469
|
|
|
2409
|
-
console.log('updated:', ${subjOne}.${
|
|
2470
|
+
console.log('updated:', ${subjOne}.${changeFields[0].name})
|
|
2410
2471
|
\`\`\`
|
|
2411
2472
|
|
|
2412
2473
|
`)
|
|
@@ -2455,7 +2516,7 @@ They behave the same way on every entity this plugin exposes.
|
|
|
2455
2516
|
|
|
2456
2517
|
if (null != child) {
|
|
2457
2518
|
const ckey = child.parents[0]
|
|
2458
|
-
const cidf =
|
|
2519
|
+
const cidf = 'id'
|
|
2459
2520
|
const cparent = null == childParent ? 'their parent' :
|
|
2460
2521
|
`${childParent.name} records`
|
|
2461
2522
|
const cop = child.cmds.includes('list') ? 'list' : 'load'
|
|
@@ -2472,7 +2533,7 @@ They behave the same way on every entity this plugin exposes.
|
|
|
2472
2533
|
` const ${plural(childParent.name)} = await seneca
|
|
2473
2534
|
.entity('${canon(childParent.name)}')
|
|
2474
2535
|
.list$()
|
|
2475
|
-
const ${ident(ckey)} = ${plural(childParent.name)}[0]
|
|
2536
|
+
const ${ident(ckey)} = ${plural(childParent.name)}[0].id
|
|
2476
2537
|
|
|
2477
2538
|
` : ''
|
|
2478
2539
|
|
|
@@ -2577,7 +2638,9 @@ the way you saw:
|
|
|
2577
2638
|
}
|
|
2578
2639
|
if (canWrite) {
|
|
2579
2640
|
Content(`- \`save$\` creates without an id and updates with one, and the
|
|
2580
|
-
${
|
|
2641
|
+
${true === subject.rkoncreate ?
|
|
2642
|
+
`id is the \`${subject.rk}\` the create sends.` :
|
|
2643
|
+
`${source} chooses the id.`}
|
|
2581
2644
|
`)
|
|
2582
2645
|
}
|
|
2583
2646
|
if (offline) {
|
|
@@ -2630,19 +2693,20 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
2630
2693
|
}
|
|
2631
2694
|
|
|
2632
2695
|
const canon = (e: any) => `provider/${provider.lower}/${e.name}`
|
|
2633
|
-
|
|
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'
|
|
2634
2701
|
|
|
2635
2702
|
// A parent key's example value. This MIRRORS seedRecord rather than
|
|
2636
2703
|
// inventing something more readable: the offline recipe below seeds with
|
|
2637
2704
|
// seedRecord, and an example id that does not match what was seeded turns
|
|
2638
2705
|
// every other recipe into a lookup that answers null.
|
|
2639
|
-
const parentVal = (e: any, k: string) =>
|
|
2640
|
-
const f = e.fields.find((f: any) => f.name === k)
|
|
2641
|
-
return null == f ? `${k.replace(/_id$/, '')}0` : `${f.parentEntity}0`
|
|
2642
|
-
}
|
|
2706
|
+
const parentVal = (e: any, k: string) => parentSeed(e, k)
|
|
2643
2707
|
|
|
2644
2708
|
const parentArgs = (e: any) =>
|
|
2645
|
-
e.parents.map((k: string) => `${k}: '${parentVal(e, k)}'`).join(', ')
|
|
2709
|
+
e.parents.map((k: string) => `${jsKey(k)}: '${parentVal(e, k)}'`).join(', ')
|
|
2646
2710
|
|
|
2647
2711
|
// A query naming ONE record. A top-level entity takes the bare id string;
|
|
2648
2712
|
// a nested one cannot, because it is identified by the whole set of keys.
|
|
@@ -2654,8 +2718,8 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
2654
2718
|
|
|
2655
2719
|
// The SDK's own entity ops always take an object, even for a bare id.
|
|
2656
2720
|
const sdkLoadArgs = (e: any) => 0 === e.parents.length ?
|
|
2657
|
-
`{ ${
|
|
2658
|
-
`{ ${parentArgs(e)}, ${
|
|
2721
|
+
`{ ${jsKey(apiKey(e))}: '${e.name}0' }` :
|
|
2722
|
+
`{ ${parentArgs(e)}, ${jsKey(apiKey(e))}: '${e.name}0' }`
|
|
2659
2723
|
|
|
2660
2724
|
const key = (k: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : `'${k}'`
|
|
2661
2725
|
|
|
@@ -2682,16 +2746,18 @@ const DocHowto = cmp(function DocHowto(props: any) {
|
|
|
2682
2746
|
|
|
2683
2747
|
// What a create sends: the seeded record without its id, because the id is
|
|
2684
2748
|
// the API's to assign. Parent keys stay — a nested write carries them in
|
|
2685
|
-
// 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.
|
|
2686
2751
|
const createData = (e: any) => {
|
|
2687
2752
|
const rec = seedRecord(e, 0)
|
|
2688
|
-
|
|
2753
|
+
if (true !== e.rkoncreate) {
|
|
2754
|
+
delete rec[apiKey(e)]
|
|
2755
|
+
}
|
|
2689
2756
|
delete rec.id
|
|
2690
2757
|
return rec
|
|
2691
2758
|
}
|
|
2692
2759
|
|
|
2693
|
-
const changeable = (e: any) => e.fields.find((f: any) =>
|
|
2694
|
-
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))
|
|
2695
2761
|
|
|
2696
2762
|
const newValue = (f: any) => 'number' === f.kind ? '999' :
|
|
2697
2763
|
'boolean' === f.kind ? 'true' : `'${f.name}-changed'`
|
|
@@ -2750,9 +2816,10 @@ const ${eLoad.name} = await seneca
|
|
|
2750
2816
|
.entity('${canon(eLoad)}')
|
|
2751
2817
|
.load$(${oneArgs(eLoad)})
|
|
2752
2818
|
\`\`\`
|
|
2753
|
-
${'id' ===
|
|
2754
|
-
The
|
|
2755
|
-
|
|
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.
|
|
2756
2823
|
`}
|
|
2757
2824
|
A record that is not there comes back as \`null\`. It is not an error and
|
|
2758
2825
|
it does not throw, so test the value rather than wrapping the call:
|
|
@@ -2969,28 +3036,59 @@ companion test server listens, so local development usually needs no
|
|
|
2969
3036
|
\`base\` at all.`}`)
|
|
2970
3037
|
|
|
2971
3038
|
|
|
2972
|
-
|
|
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
|
|
2973
3067
|
convention, so that every provider in an application is configured the
|
|
2974
3068
|
same way. Declare the variable with \`env\` and set the key under this
|
|
2975
3069
|
provider's name:
|
|
2976
3070
|
|
|
2977
3071
|
\`\`\`js
|
|
2978
3072
|
.use('env', {
|
|
2979
|
-
var: { $${provider.ENV}_APIKEY: String
|
|
3073
|
+
var: { $${provider.ENV}_APIKEY: String${provider.authBasic ?
|
|
3074
|
+
`, $${provider.ENV}_SECRET: String` : ''} },
|
|
2980
3075
|
})
|
|
2981
3076
|
.use('provider', {
|
|
2982
3077
|
provider: {
|
|
2983
3078
|
${provider.lower}: {
|
|
2984
3079
|
keys: {
|
|
2985
|
-
apikey: { value: '$${provider.ENV}_APIKEY' }
|
|
3080
|
+
apikey: { value: '$${provider.ENV}_APIKEY' },${provider.authBasic ? `
|
|
3081
|
+
secret: { value: '$${provider.ENV}_SECRET' },` : ''}
|
|
2986
3082
|
},
|
|
2987
3083
|
},
|
|
2988
3084
|
},
|
|
2989
3085
|
})
|
|
2990
3086
|
\`\`\`
|
|
2991
3087
|
|
|
2992
|
-
Every request then carries
|
|
2993
|
-
|
|
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
|
|
2994
3092
|
is configured in exactly the same shape with an empty value — which is
|
|
2995
3093
|
why it is worth writing even when there is nothing to send. An
|
|
2996
3094
|
application that later moves to an authenticated service then changes one
|
|
@@ -3004,6 +3102,7 @@ For a different scheme, set the header yourself. Headers supplied through
|
|
|
3004
3102
|
sdk: { headers: { 'x-api-key': process.env.${provider.ENV}_APIKEY } },
|
|
3005
3103
|
})
|
|
3006
3104
|
\`\`\``)
|
|
3105
|
+
}
|
|
3007
3106
|
|
|
3008
3107
|
|
|
3009
3108
|
sec('Check which plugin and SDK are running', `One message, and the thing to reach for when a deployment is behaving
|
|
@@ -3032,7 +3131,7 @@ released separately and most surprises live in the gap between them.`)
|
|
|
3032
3131
|
const dpe = eList || subject
|
|
3033
3132
|
const dpath = dpe.path || provider.probePath || '/'
|
|
3034
3133
|
const dparams = pathParams(dpath)
|
|
3035
|
-
const dval = (k: string) => (k ===
|
|
3134
|
+
const dval = (k: string) => (k === apiKey(dpe) || 'id' === k) ?
|
|
3036
3135
|
`${dpe.name}0` : `${k.replace(/_id$/, '')}0`
|
|
3037
3136
|
|
|
3038
3137
|
sec('Reach the SDK directly', `The entity API covers the operations the API model declares. For
|
|
@@ -3305,12 +3404,14 @@ const DocReference = cmp(function DocReference(props: any) {
|
|
|
3305
3404
|
// A query literal for the docs: parent keys first, then whatever else the
|
|
3306
3405
|
// command needs.
|
|
3307
3406
|
const query = (e: any, extra: string[]) =>
|
|
3308
|
-
`{ ${[...e.parents, ...extra].map((k: string) => `${k}: '...'`).join(', ')} }`
|
|
3407
|
+
`{ ${[...e.parents, ...extra].map((k: string) => `${jsKey(k)}: '...'`).join(', ')} }`
|
|
3309
3408
|
|
|
3310
3409
|
// How a single record is addressed. The `load$('x')` short form only works
|
|
3311
3410
|
// when the id field is literally `id`.
|
|
3312
|
-
const oneArg = (e: any) => 0 < e.parents.length ? query(e, [
|
|
3313
|
-
|
|
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')
|
|
3314
3415
|
|
|
3315
3416
|
// The required-key phrasing, which has to read correctly for one key as
|
|
3316
3417
|
// well as several.
|
|
@@ -3477,11 +3578,11 @@ A canon carries only the commands its API operations support — an entity the
|
|
|
3477
3578
|
API offers no delete for has no \`remove$\` — so the tables below are the
|
|
3478
3579
|
whole of what each one answers.
|
|
3479
3580
|
|
|
3480
|
-
| Seneca canon | SDK accessor | Route |
|
|
3481
|
-
| ------------ | ------------ | ----- |
|
|
3581
|
+
| Seneca canon | SDK accessor | Route | API key | Parent keys | Commands |
|
|
3582
|
+
| ------------ | ------------ | ----- | ------- | ----------- | -------- |
|
|
3482
3583
|
`)
|
|
3483
3584
|
each(provider.entities, (e: any) => {
|
|
3484
|
-
Content(`| \`${canon(e)}\` | \`sdk.${e.acc}()\` | \`${e.path}\` | \`${e
|
|
3585
|
+
Content(`| \`${canon(e)}\` | \`sdk.${e.acc}()\` | \`${e.path}\` | \`${apiKeyOf(e)}\` | ${0 < e.parents.length ?
|
|
3485
3586
|
keys(e.parents) : '—'} | ${cmdList(e)} |
|
|
3486
3587
|
`)
|
|
3487
3588
|
})
|
|
@@ -3517,7 +3618,7 @@ before any request is made, rather than issuing one that would 404.
|
|
|
3517
3618
|
`)
|
|
3518
3619
|
}
|
|
3519
3620
|
if (e.cmds.includes('load')) {
|
|
3520
|
-
Content(`| \`load$(q)\` | ${reqd([...e.parents,
|
|
3621
|
+
Content(`| \`load$(q)\` | ${reqd([...e.parents, 'id'])} | One \`${e.name}\`, or \`null\` if not found. |
|
|
3521
3622
|
`)
|
|
3522
3623
|
}
|
|
3523
3624
|
if (e.cmds.includes('save')) {
|
|
@@ -3529,24 +3630,19 @@ before any request is made, rather than issuing one that would 404.
|
|
|
3529
3630
|
`)
|
|
3530
3631
|
}
|
|
3531
3632
|
if (e.cmds.includes('remove')) {
|
|
3532
|
-
Content(`| \`remove$(q)\` | ${reqd([...e.parents,
|
|
3633
|
+
Content(`| \`remove$(q)\` | ${reqd([...e.parents, 'id'])} | \`null\`. |
|
|
3533
3634
|
`)
|
|
3534
3635
|
}
|
|
3535
3636
|
|
|
3536
|
-
|
|
3537
|
-
// anything else never reads. Nested entities need the object form for
|
|
3538
|
-
// their parent keys anyway, so this only needs saying for top-level ones.
|
|
3539
|
-
const shortForm = 0 === e.parents.length && 'id' !== e.idf ?
|
|
3540
|
-
e.cmds.filter((c: string) => 'load' === c || 'remove' === c) : []
|
|
3541
|
-
|
|
3542
|
-
if (0 < shortForm.length) {
|
|
3637
|
+
if ('id' !== apiKeyOf(e)) {
|
|
3543
3638
|
Content(`
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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\`.`}
|
|
3550
3646
|
`)
|
|
3551
3647
|
}
|
|
3552
3648
|
|
|
@@ -3565,7 +3661,8 @@ also defines are passed through unchanged in both directions.
|
|
|
3565
3661
|
| ----- | ---- | ----- |
|
|
3566
3662
|
`)
|
|
3567
3663
|
each(e.fields, (f: any) => {
|
|
3568
|
-
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`.') :
|
|
3569
3666
|
e.parents.includes(f.name) ? ('' === f.parentEntity ?
|
|
3570
3667
|
'Parent key. Required by every command.' :
|
|
3571
3668
|
`Parent key: the id of a \`${f.parentEntity}\`. Required by every command.`) : ''} |
|
|
@@ -3598,15 +3695,14 @@ also defines are passed through unchanged in both directions.
|
|
|
3598
3695
|
// The dispatching entity to show it with: the subject when it qualifies,
|
|
3599
3696
|
// otherwise the first that does.
|
|
3600
3697
|
const s = dispatch.includes(subject) ? subject : dispatch[0]
|
|
3601
|
-
const writable = s.fields
|
|
3602
|
-
|
|
3603
|
-
.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))
|
|
3604
3700
|
const value = (f: any, alt: boolean) => 'number' === f.kind ?
|
|
3605
3701
|
(alt ? '4321' : '1234') : 'boolean' === f.kind ?
|
|
3606
3702
|
(alt ? 'true' : 'false') : `'${f.name}${alt ? '-changed' : '-value'}'`
|
|
3607
3703
|
const make = [
|
|
3608
|
-
...s.parents.map((k: string) => `${k}: '...'`),
|
|
3609
|
-
...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)}`),
|
|
3610
3706
|
].join(', ')
|
|
3611
3707
|
|
|
3612
3708
|
Content(`
|
|
@@ -3617,14 +3713,14 @@ created, an entity **with** one is updated. The provider dispatches on the
|
|
|
3617
3713
|
id field, so the same call does both.
|
|
3618
3714
|
|
|
3619
3715
|
\`\`\`js
|
|
3620
|
-
// Create — no
|
|
3716
|
+
// Create — no id.
|
|
3621
3717
|
const ${s.name} = await seneca
|
|
3622
3718
|
.entity('${canon(s)}')
|
|
3623
3719
|
.make$({ ${make} })
|
|
3624
3720
|
.save$()
|
|
3625
3721
|
|
|
3626
|
-
// Update —
|
|
3627
|
-
${0 <
|
|
3722
|
+
// Update — id present.
|
|
3723
|
+
${0 < alterable.length ? `${s.name}.${alterable[0].name} = ${value(alterable[0], true)}
|
|
3628
3724
|
` : ''}await ${s.name}.save$()
|
|
3629
3725
|
\`\`\`
|
|
3630
3726
|
|
|
@@ -3876,19 +3972,26 @@ it is absent, \`null\` or the empty string.
|
|
|
3876
3972
|
|
|
3877
3973
|
Content(`
|
|
3878
3974
|
## Authentication keys
|
|
3879
|
-
|
|
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
|
+
` : `
|
|
3880
3981
|
The plugin follows the provider convention: if an \`apikey\` key is
|
|
3881
|
-
configured and non-empty, it is sent as
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
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.
|
|
3885
3987
|
|
|
3886
3988
|
\`\`\`js
|
|
3887
3989
|
.use('provider', {
|
|
3888
3990
|
provider: {
|
|
3889
3991
|
${provider.lower}: {
|
|
3890
3992
|
keys: {
|
|
3891
|
-
apikey: { value: '$${provider.ENV}_APIKEY' }
|
|
3993
|
+
apikey: { value: '$${provider.ENV}_APIKEY' },${provider.authBasic ? `
|
|
3994
|
+
secret: { value: '$${provider.ENV}_SECRET' },` : ''}
|
|
3892
3995
|
},
|
|
3893
3996
|
},
|
|
3894
3997
|
},
|
|
@@ -3896,9 +3999,10 @@ no credential exercises the same path.
|
|
|
3896
3999
|
\`\`\`
|
|
3897
4000
|
|
|
3898
4001
|
The key is read once, during \`seneca.prepare()\`, by posting
|
|
3899
|
-
\`sys:provider,get:keymap,provider:${provider.lower}\`.
|
|
3900
|
-
|
|
3901
|
-
|
|
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
|
+
`}
|
|
3902
4006
|
## Environment variables
|
|
3903
4007
|
|
|
3904
4008
|
The plugin never reads the environment itself. These are the variables the
|
|
@@ -4287,12 +4391,19 @@ by hand. Nothing about the mapping is waiting to be written.
|
|
|
4287
4391
|
`)
|
|
4288
4392
|
}
|
|
4289
4393
|
|
|
4290
|
-
Content(`## Credentials, whether or not the API needs them
|
|
4394
|
+
Content(provider.authActive ? `## Credentials, whether or not the API needs them
|
|
4291
4395
|
|
|
4292
4396
|
At startup the plugin asks \`@seneca/provider\` for the keymap of
|
|
4293
|
-
\`${provider.lower}\` and sends the \`apikey\` as
|
|
4294
|
-
configured.
|
|
4397
|
+
\`${provider.lower}\` and sends the \`apikey\` as ${credentialWire(provider)}
|
|
4398
|
+
when one is configured.
|
|
4399
|
+
` : `## Credentials, for an API that declares none
|
|
4295
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(`
|
|
4296
4407
|
The key is *optional*. Absent, unconfigured and empty all mean "send no
|
|
4297
4408
|
header", and none of them is an error. For an API that needs no credential this
|
|
4298
4409
|
looks like ceremony, and it is worth keeping anyway: the shape of a Seneca
|