@voxgig/sdkgen-infrapack 0.0.13 → 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.
@@ -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
- .map((p: string) => live ? `${p}, ` : `${p}: '${parentSeed(e, p)}', `)
86
- .join('')
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].${pe.idf || 'id'}
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.name !== e.idf && 'id' !== f.name &&
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
- const mut = mutableField(e)
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. The API
324
- ${ind} // assigns the id itself, so the saved record comes back with one it chose.
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.${e.idf || 'id'}, '${entIdLiteral(e, '0')}')
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.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&')
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].${key}, '${parentSeed(e, key)}')`}
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 (compositeRoundTrip(e) && removeAddresses(e) &&
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.name}0' })
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.name}0' })
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
- if (subject.cmds.includes('load')) {
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$('nosuch${subject.name}'),
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].${parent.idf || 'id'} }))
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 = subject.idf || 'id'
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: the API assigns the id, so none is supplied here.
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 = child.idf || 'id'
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 = subject.idf || 'id'
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.idf || 'id'
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].${subjParent.idf || 'id'}
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
- f.name !== idf && 'id' !== f.name && !subject.parents.includes(f.name))
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 — an empty
2291
- value simply means no \`authorization\` header is sent. Every Seneca
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
- Run it, and note the id printed. It is **not** one you chose — the
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}.${plainFields[0].name} = ${litval(plainFields[0], true)}
2467
+ ${subjOne}.${changeFields[0].name} = ${litval(changeFields[0], true)}
2407
2468
  ${subjOne} = await ${subjOne}.save$()
2408
2469
 
2409
- console.log('updated:', ${subjOne}.${plainFields[0].name})
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 = child.idf || 'id'
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].${childParent.idf || 'id'}
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
- ${source} chooses the id.
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
- const idf = (e: any) => e.idf || 'id'
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
- `{ ${idf(e)}: '${e.name}0' }` :
2658
- `{ ${parentArgs(e)}, ${idf(e)}: '${e.name}0' }`
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
- delete rec[idf(e)]
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' === idf(eLoad) ? '' : `
2754
- The id field for \`${eLoad.name}\` is \`${idf(eLoad)}\`, so that is the
2755
- key to supply.
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
- sec('Send an API key', `Credentials are not a plugin option: they come through the provider
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 \`authorization: Bearer <apikey>\`. An absent
2993
- or empty key adds no header at all, so an API that needs no credentials
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 === idf(dpe) || 'id' === 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, [e.idf]) :
3313
- 'id' === e.idf ? `'...'` : query(e, [e.idf])
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 | Id field | Parent keys | Commands |
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.idf}\` | ${0 < e.parents.length ?
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, e.idf])} | One \`${e.name}\`, or \`null\` if not found. |
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, e.idf])} | \`null\`. |
3633
+ Content(`| \`remove$(q)\` | ${reqd([...e.parents, 'id'])} | \`null\`. |
3533
3634
  `)
3534
3635
  }
3535
3636
 
3536
- // The `load$('x')` short form sets `id`, which an entity keyed by
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
- This entity is keyed by \`${e.idf}\` rather than \`id\`, so the short
3545
- ${1 === shortForm.length ? 'form' : 'forms'} ${shortForm
3546
- .map((c: string) => `\`${c}$('...')\``).join(' and ')} ${1 === shortForm.length ?
3547
- 'does' : 'do'} not address it: Seneca reads a bare string as
3548
- \`{id: '...'}\`, which is not a key this entity uses. Pass
3549
- \`{ ${e.idf}: '...' }\` instead.
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.idf ? 'Id field.' :
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
- .filter((f: any) => f.name !== s.idf && f.name !== 'id')
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 ${s.idf}.
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 — ${s.idf} present.
3627
- ${0 < writable.length ? `${s.name}.${writable[0].name} = ${value(writable[0], true)}
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 \`authorization: Bearer <apikey>\`
3882
- on every request. If the provider is not registered, or the key is absent or
3883
- empty, no header is added and startup proceeds normally — an API that needs
3884
- no credential exercises the same path.
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}\`. An \`authorization\`
3900
- header supplied through the \`sdk.headers\` option takes precedence over it.
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 a bearer token when one is
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