@voxgig/sdkgen-infrapack 0.0.3 → 0.0.5
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.
|
@@ -156,6 +156,42 @@ function requiredKeys(ent: any, opname: string): string[] {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
// The keys a single-record call actually sends: the op's required keys, plus
|
|
160
|
+
// the record's own key when the match declares it at all. One definition,
|
|
161
|
+
// because the handler emitter and the test emitter disagreeing about it is
|
|
162
|
+
// how a test comes to assert something the generated code never does.
|
|
163
|
+
function addressKeys(ent: any, opname: string): string[] {
|
|
164
|
+
const keys = requiredKeys(ent, opname)
|
|
165
|
+
const parts = idParts(ent)
|
|
166
|
+
|
|
167
|
+
if (0 < parts.length) {
|
|
168
|
+
// EVERY PART, required or not — for the same reason a single record key
|
|
169
|
+
// always travels. github's api_insights_summary_stat is keyed
|
|
170
|
+
// `actor_type/actor_id` and declares both OPTIONAL, because the op also
|
|
171
|
+
// covers routes that take neither; only `min_timestamp` came through as
|
|
172
|
+
// required. So the call sent the timestamp alone, the split id went
|
|
173
|
+
// nowhere, and every read answered with the same record — the composite
|
|
174
|
+
// half of the defect fixed below for single keys.
|
|
175
|
+
const shape = opRequestShape(ent, opname).items
|
|
176
|
+
for (const part of parts) {
|
|
177
|
+
if (!keys.includes(part) &&
|
|
178
|
+
shape.some((it: any) => it.name === part)) {
|
|
179
|
+
keys.push(part)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return keys
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rk = recordKey(ent)
|
|
186
|
+
if (null != rk && '' !== rk && !keys.includes(rk) &&
|
|
187
|
+
opRequestShape(ent, opname).items.some((it: any) => it.name === rk)) {
|
|
188
|
+
keys.push(rk)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return keys
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
159
195
|
// The key that addresses ONE record.
|
|
160
196
|
//
|
|
161
197
|
// `entityIdField` answers whenever the model declares one, which apidef does
|
|
@@ -176,6 +212,60 @@ function requiredKeys(ent: any, opname: string): string[] {
|
|
|
176
212
|
// record (base_id, table_id, record_id) alphabetizes with table_id last,
|
|
177
213
|
// so the old `params[params.length - 1]` picked the wrong parent as the
|
|
178
214
|
// record's own key. The point's own `parts` still has the true order.
|
|
215
|
+
// The path parameters that TOGETHER name one record, when no single one does.
|
|
216
|
+
//
|
|
217
|
+
// apidef sets `id.parts` for an API that addresses a record by several
|
|
218
|
+
// adjacent path parameters — github needs {owner} AND {repo} to name a
|
|
219
|
+
// repository — and `id.sep` (a slash) joins them into the ONE id a Seneca
|
|
220
|
+
// entity carries. Empty for the ordinary single-key entity, which is most of
|
|
221
|
+
// them, so every caller can branch on `0 < parts.length`.
|
|
222
|
+
function idParts(ent: any): string[] {
|
|
223
|
+
const parts = ent?.id?.parts
|
|
224
|
+
return Array.isArray(parts) && 1 < parts.length ?
|
|
225
|
+
parts.map((p: any) => String(p)) : []
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
function idSep(ent: any): string {
|
|
230
|
+
const sep = ent?.id?.sep
|
|
231
|
+
return null != sep && '' !== String(sep) ? String(sep) : '/'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// THE ONE DESCRIPTION OF AN ENTITY'S ID, or null when Seneca's `id` and the
|
|
236
|
+
// API's key already agree and nothing needs translating.
|
|
237
|
+
//
|
|
238
|
+
// It unifies the two cases that used to be handled by separate emitters. A
|
|
239
|
+
// compound key (`{owner}/{repo}`) has several parts; an API that simply calls
|
|
240
|
+
// its key something else (`repo`, `number`) has exactly one. Neither needs a
|
|
241
|
+
// different algorithm — a one-part split is the identity, and a one-part join
|
|
242
|
+
// is the old carry-across — so both come through the same table and the same
|
|
243
|
+
// two functions.
|
|
244
|
+
//
|
|
245
|
+
// `from` is passed through as the model states it: which response field
|
|
246
|
+
// carries each part. apidef derives it and guide.aon can correct it.
|
|
247
|
+
function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string, string> } | null {
|
|
248
|
+
const parts = idParts(ent)
|
|
249
|
+
if (0 < parts.length) {
|
|
250
|
+
const from = ent?.id?.from
|
|
251
|
+
return {
|
|
252
|
+
parts,
|
|
253
|
+
sep: idSep(ent),
|
|
254
|
+
...(null != from && 'object' === typeof from ? { from } : {}),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rk = recordKey(ent)
|
|
259
|
+
if ('id' === rk) {
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// A single-key entity whose key is not `id`. `from` defaults to the key's
|
|
264
|
+
// own name, which is what the old carry-across read.
|
|
265
|
+
return { parts: [rk], sep: idSep(ent) }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
179
269
|
function recordKey(ent: any): string {
|
|
180
270
|
const idf = entityIdField(ent)
|
|
181
271
|
if (null != idf && '' !== idf) {
|
|
@@ -434,6 +524,77 @@ const Main = cmp(function Main(props: any) {
|
|
|
434
524
|
cls: entityClassName(ent, entityColl),
|
|
435
525
|
ops: entityOps(ent),
|
|
436
526
|
idf: entityIdField(ent),
|
|
527
|
+
// The field this API's routes actually ADDRESS a record by, which is
|
|
528
|
+
// not always `id` and is not always what entityIdField answers (that
|
|
529
|
+
// returns null when the load match has no `id`, leaving recordKey to
|
|
530
|
+
// read the route's last variable segment). The doc and test emitters
|
|
531
|
+
// in Extras need the same answer the handler emitters use, or the
|
|
532
|
+
// seed they build is keyed by a field the routes never look at.
|
|
533
|
+
rk: recordKey(ent),
|
|
534
|
+
// The composite key, when this API addresses a record by several
|
|
535
|
+
// path params at once. The doc and test emitters in Extras need the
|
|
536
|
+
// same answer the handler emitters use: a test that addresses a
|
|
537
|
+
// composite entity the single-key way builds a query its own handler
|
|
538
|
+
// rejects.
|
|
539
|
+
idparts: idParts(ent),
|
|
540
|
+
idsep: idSep(ent),
|
|
541
|
+
// DOES EACH SINGLE-RECORD OP ACTUALLY CARRY THE RECORD'S KEY?
|
|
542
|
+
//
|
|
543
|
+
// Only then can a wrong id miss on a LOAD: github's `interaction`
|
|
544
|
+
// reads `/user/interaction-limits` — a singleton, called as
|
|
545
|
+
// `load({})` — so `load$('no-such-id')` correctly returns the one
|
|
546
|
+
// record there is, and a not-found test against it asserts the
|
|
547
|
+
// opposite of the truth.
|
|
548
|
+
//
|
|
549
|
+
// And only then can a REMOVE delete what a create just made. A
|
|
550
|
+
// tag-bucket entity can have ops addressing different resources
|
|
551
|
+
// entirely: github's `action` is keyed `archive_format` from its
|
|
552
|
+
// download route while its remove takes `hosted_runner_id` and
|
|
553
|
+
// `org_id`, so the remove addressed by parent scope alone — it
|
|
554
|
+
// deleted whichever record the store happened to yield first, which
|
|
555
|
+
// was usually a SEEDED one, and the round-trip failed on the record
|
|
556
|
+
// it had created surviving. Intermittently: the created record's id
|
|
557
|
+
// is random, so where it falls in iteration order decides.
|
|
558
|
+
//
|
|
559
|
+
// Parent keys alone do not distinguish records, which is why this
|
|
560
|
+
// asks for the record's key or every composite part rather than
|
|
561
|
+
// merely for "the route has a parameter".
|
|
562
|
+
// WHICH SINGLE-RECORD OPS ADDRESS A DIFFERENT RESOURCE ENTIRELY.
|
|
563
|
+
//
|
|
564
|
+
// Not merely "cannot address the record": an op that addresses
|
|
565
|
+
// NOTHING is a singleton read, and github's `interaction`
|
|
566
|
+
// (`/user/interaction-limits`) is a real one. This is the other case
|
|
567
|
+
// — the op addresses something, and it is not this record. Every one
|
|
568
|
+
// is a tag-derived entity whose ops were gathered from unrelated
|
|
569
|
+
// routes: `migration`'s remove takes `owner` and `repo` and deletes
|
|
570
|
+
// a repository's migration archive, `user`'s deletes a GPG KEY, and
|
|
571
|
+
// `pull`'s deletes a review COMMENT. The id the caller passed is
|
|
572
|
+
// dropped, and the request goes anyway.
|
|
573
|
+
//
|
|
574
|
+
// Reads are untouched across the whole of github — this is 7 removes
|
|
575
|
+
// and 5 updates, and both are writes.
|
|
576
|
+
idmisaddressed: ['remove', 'update'].reduce(
|
|
577
|
+
(acc: Record<string, boolean>, opname: string) => {
|
|
578
|
+
acc[opname] = null != (ent.op || {})[opname] &&
|
|
579
|
+
0 < addressKeys(ent, opname).length &&
|
|
580
|
+
!(0 < idParts(ent).length ?
|
|
581
|
+
idParts(ent).every((p: string) =>
|
|
582
|
+
addressKeys(ent, opname).includes(p)) :
|
|
583
|
+
addressKeys(ent, opname).includes(recordKey(ent)))
|
|
584
|
+
return acc
|
|
585
|
+
}, {}),
|
|
586
|
+
idaddressed: ['load', 'remove', 'update'].reduce(
|
|
587
|
+
(acc: Record<string, boolean>, opname: string) => {
|
|
588
|
+
const keys = addressKeys(ent, opname)
|
|
589
|
+
const parts = idParts(ent)
|
|
590
|
+
acc[opname] = 0 < parts.length ?
|
|
591
|
+
parts.every((p: string) => keys.includes(p)) :
|
|
592
|
+
keys.includes(recordKey(ent))
|
|
593
|
+
return acc
|
|
594
|
+
}, {}),
|
|
595
|
+
// Where each part is carried in a response. The test emitter needs
|
|
596
|
+
// it to know whether a created record's id can be rebuilt at all.
|
|
597
|
+
idfrom: (ent?.id?.from) || {},
|
|
437
598
|
parents,
|
|
438
599
|
parentOf,
|
|
439
600
|
opParents,
|
|
@@ -550,6 +711,11 @@ const Main = cmp(function Main(props: any) {
|
|
|
550
711
|
const provider = {
|
|
551
712
|
Name, lower, ENV, sdkClass, pluginName, fileBase,
|
|
552
713
|
sdkPkg, sdkVersion, entities,
|
|
714
|
+
// Whether the SDK dependency is a git tag rather than a registry
|
|
715
|
+
// package. The generated CI note says which, because "npm install is all
|
|
716
|
+
// you need" stops being true the moment git is in the path.
|
|
717
|
+
sdkGit: sdkDependency(model, target, { sdkVersion, sdkRepoUrl: repoInfo(model).repoUrl })
|
|
718
|
+
.startsWith('github:'),
|
|
553
719
|
repoUrl: repo.url,
|
|
554
720
|
// The SDK's own repo, for pointing at the companion test server which is
|
|
555
721
|
// only distributed in source.
|
|
@@ -611,6 +777,74 @@ const Main = cmp(function Main(props: any) {
|
|
|
611
777
|
})
|
|
612
778
|
|
|
613
779
|
|
|
780
|
+
// HOW THE PROVIDER DEPENDS ON THE SDK IT WRAPS, as one dependency value.
|
|
781
|
+
//
|
|
782
|
+
// Default: the PUBLISHED package pinned to the version the `ts` target
|
|
783
|
+
// publishes, so the two can never disagree. Right whenever the SDK is on a
|
|
784
|
+
// registry — and wrong when it is not. An SDK for a private API, or one not
|
|
785
|
+
// published yet, leaves the provider unable to `npm install` at all: the
|
|
786
|
+
// dependency 404s, so the package cannot be built, tested or released. That
|
|
787
|
+
// is not hypothetical; it is why @seneca/github-provider could not be
|
|
788
|
+
// regenerated and released for weeks.
|
|
789
|
+
//
|
|
790
|
+
// `kind: 'git'` points at a GIT TAG instead, which needs no registry.
|
|
791
|
+
//
|
|
792
|
+
// NPM RESOLVES A GIT DEPENDENCY AGAINST THE REPOSITORY ROOT, and sdkgen
|
|
793
|
+
// generates the TypeScript SDK into `ts/` — so the bare
|
|
794
|
+
// `github:owner/repo#ref` every example shows would install a directory with
|
|
795
|
+
// no package.json in it. npm spells the subdirectory `#<ref>::path:<sub>`
|
|
796
|
+
// (npm-package-arg resolves that to gitSubdir), and `path` therefore
|
|
797
|
+
// defaults to `ts` rather than to nothing: the default has to match the
|
|
798
|
+
// layout this toolchain actually produces, or the shorthand is a trap. `.`
|
|
799
|
+
// means the package IS the repository root.
|
|
800
|
+
//
|
|
801
|
+
// `spec` still wins over all of it, for anything the shorthand cannot say.
|
|
802
|
+
function sdkDependency(model: any, target: any, provider: any): string {
|
|
803
|
+
const dep = model?.main?.[KIT]?.target?.[target.name]?.sdk?.dep || {}
|
|
804
|
+
|
|
805
|
+
const spec = String(dep.spec || '').trim()
|
|
806
|
+
if ('' !== spec) {
|
|
807
|
+
return spec
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if ('git' !== String(dep.kind || 'npm')) {
|
|
811
|
+
return `^${provider.sdkVersion}`
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// `owner/repo`, from the SDK's own repository unless the project says
|
|
815
|
+
// otherwise. Accepts a full URL and reduces it, so a project can paste
|
|
816
|
+
// what its remote prints.
|
|
817
|
+
const repo = String(dep.repo || provider.sdkRepoUrl || '')
|
|
818
|
+
.replace(/^git\+/, '')
|
|
819
|
+
.replace(/^(https?:\/\/)?(www\.)?github\.com[/:]/, '')
|
|
820
|
+
.replace(/\.git$/, '')
|
|
821
|
+
.replace(/\/+$/, '')
|
|
822
|
+
|
|
823
|
+
if ('' === repo) {
|
|
824
|
+
throw new SdkGenError(
|
|
825
|
+
'seneca-provider: sdk.dep.kind is "git" but no repository is known. ' +
|
|
826
|
+
'Set `main.' + KIT + '.target.' + target.name +
|
|
827
|
+
'.sdk.dep.repo` to `owner/repo`, or state the whole dependency with ' +
|
|
828
|
+
'`sdk.dep.spec`.')
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const ref = String(dep.ref || '').trim()
|
|
832
|
+
if ('' === ref) {
|
|
833
|
+
throw new SdkGenError(
|
|
834
|
+
'seneca-provider: sdk.dep.kind is "git" but no `ref` is set. A git ' +
|
|
835
|
+
'dependency with no ref follows the default branch, so an install ' +
|
|
836
|
+
'today and an install tomorrow can differ — name the TAG to depend ' +
|
|
837
|
+
'on, e.g. `sdk.dep.ref: "v' + provider.sdkVersion + '"`.')
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// `.` (and empty) mean the repository root, which needs no path segment.
|
|
841
|
+
const sub = String(dep.path ?? 'ts').trim().replace(/^\/+|\/+$/g, '')
|
|
842
|
+
|
|
843
|
+
return `github:${repo}#${ref}` +
|
|
844
|
+
('' === sub || '.' === sub ? '' : `::path:${sub}`)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
|
|
614
848
|
// --- package.json -----------------------------------------------------------
|
|
615
849
|
|
|
616
850
|
const PackageJson = cmp(function PackageJson(props: any) {
|
|
@@ -699,11 +933,22 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
699
933
|
},
|
|
700
934
|
// What actually ships. Without `files`, `npm publish` packs the test
|
|
701
935
|
// suite and build output into the tarball.
|
|
702
|
-
|
|
936
|
+
//
|
|
937
|
+
// `doc` IS PART OF THE PACKAGE. The generated README links to
|
|
938
|
+
// doc/tutorial.md, doc/how-to.md, doc/reference.md and
|
|
939
|
+
// doc/explanation.md with relative paths, so omitting it published a
|
|
940
|
+
// README whose every documentation link 404s for anyone reading the
|
|
941
|
+
// installed package rather than the repository. Either the links become
|
|
942
|
+
// absolute repository URLs or the files ship; they ship, because the
|
|
943
|
+
// docs describe the exact version installed and a URL would drift to
|
|
944
|
+
// whatever main says later.
|
|
945
|
+
files: ['dist', 'doc', 'src/**/*.ts', 'LICENSE'],
|
|
703
946
|
engines: { node: '>=24' },
|
|
704
947
|
dependencies: {
|
|
705
|
-
// The SDK this plugin wraps
|
|
706
|
-
|
|
948
|
+
// The SDK this plugin wraps. Published-and-pinned by default; a git
|
|
949
|
+
// tag when the project says so, because an unpublished SDK otherwise
|
|
950
|
+
// leaves this package unable to install at all. See sdkDependency.
|
|
951
|
+
[provider.sdkPkg]: sdkDependency(model, target, provider),
|
|
707
952
|
...dep('prod'),
|
|
708
953
|
},
|
|
709
954
|
peerDependencies: dep('peer'),
|
|
@@ -892,21 +1137,136 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
892
1137
|
// needs translating in both directions, or `load$` requests a record
|
|
893
1138
|
// keyed `undefined` and every entity handed back has no id at all — one
|
|
894
1139
|
// that cannot then be saved or removed.
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1140
|
+
// ID TRANSLATION, ONE ALGORITHM AND A TABLE.
|
|
1141
|
+
//
|
|
1142
|
+
// Seneca entities carry exactly one `id`. Plenty of APIs do not: they
|
|
1143
|
+
// key a record by a differently-named field (`repo`, `number`), or by
|
|
1144
|
+
// SEVERAL path parameters at once with no single one that is the id
|
|
1145
|
+
// (github's `/repos/{owner}/{repo}`). Both need translating in both
|
|
1146
|
+
// directions, or `load$` asks for a record keyed `undefined` and every
|
|
1147
|
+
// record handed back has no id to save or remove it by.
|
|
1148
|
+
//
|
|
1149
|
+
// The LOGIC is the same for every API; only which parameters, which
|
|
1150
|
+
// separator and where they live in a response differ, and those are
|
|
1151
|
+
// model data. So this emits one `splitid`, one `joinid` and a table —
|
|
1152
|
+
// not a bespoke pair of functions per entity, which is what it used to
|
|
1153
|
+
// do and which duplicated the same algorithm N times over.
|
|
1154
|
+
const translated = provider.entities.filter((e: any) => null != idSpec(e.ent))
|
|
1155
|
+
|
|
1156
|
+
if (0 < translated.length) {
|
|
1157
|
+
const rows = translated.map((e: any) => {
|
|
1158
|
+
const spec: any = idSpec(e.ent)
|
|
1159
|
+
const from = null == spec.from ? '' :
|
|
1160
|
+
`, from: { ${Object.keys(spec.from).sort()
|
|
1161
|
+
.map((k: string) => `${jsKey(k)}: '${spec.from[k]}'`).join(', ')} }`
|
|
1162
|
+
return ` ${jsKey(e.name)}: { parts: [${
|
|
1163
|
+
spec.parts.map((p: string) => `'${p}'`).join(', ')}], sep: '${spec.sep}'${from} },`
|
|
1164
|
+
}).join('\n')
|
|
1165
|
+
|
|
1166
|
+
Content(` // HOW EACH ENTITY'S id MAPS TO THE API'S OWN KEYS, from the model.
|
|
1167
|
+
//
|
|
1168
|
+
// \`parts\` the path parameters that address one record, in path order.
|
|
1169
|
+
// One part is the ordinary case: the API just calls its key
|
|
1170
|
+
// something other than \`id\`. Two or more is a compound key,
|
|
1171
|
+
// where no single parameter names the record.
|
|
1172
|
+
// \`sep\` joins the parts into the one id a Seneca entity carries. A
|
|
1173
|
+
// slash cannot occur inside a path segment, so the join is
|
|
1174
|
+
// unambiguous and the split cannot over-split.
|
|
1175
|
+
// \`from\` where each part's value lives in a RESPONSE, as a dotted
|
|
1176
|
+
// path. A path parameter's name is not generally a response
|
|
1177
|
+
// field's name: github returns a repo's owner as an OBJECT
|
|
1178
|
+
// (\`owner.login\`) and its name as \`name\`, never \`repo\`.
|
|
1179
|
+
// A part missing here cannot be read back off a response.
|
|
1180
|
+
const ID_SPEC: Record<string, { parts: string[], sep: string, from?: Record<string, string> }> = {
|
|
1181
|
+
${rows}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
// Read a dotted path out of a record. \`from\` maps a path parameter to
|
|
1186
|
+
// wherever the response actually carries it, and that is sometimes inside
|
|
1187
|
+
// a nested object.
|
|
1188
|
+
function idread(data: any, path: string) {
|
|
1189
|
+
let node: any = data
|
|
1190
|
+
for (const key of path.split('.')) {
|
|
1191
|
+
if (null == node) {
|
|
1192
|
+
return undefined
|
|
1193
|
+
}
|
|
1194
|
+
node = node[key]
|
|
1195
|
+
}
|
|
1196
|
+
return node
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
// The Seneca id, split back into the parameters the API addresses a record
|
|
1201
|
+
// with. Refuses a wrong part count rather than sending a URL built from
|
|
1202
|
+
// whatever the id happened to contain — that would address a different
|
|
1203
|
+
// record, or none, and the 404 would name nothing useful.
|
|
1204
|
+
function splitid(name: string, id: any, what: string) {
|
|
1205
|
+
const spec = ID_SPEC[name]
|
|
1206
|
+
const text = null == id ? '' : String(id)
|
|
1207
|
+
const got = 1 === spec.parts.length ? [text] : text.split(spec.sep)
|
|
1208
|
+
|
|
1209
|
+
if (spec.parts.length !== got.length || got.some((p: string) => '' === p)) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
'${provider.pkgName}: ' + name + ' ' + what +
|
|
1212
|
+
": id must be '" + spec.parts.join(spec.sep) + "', got: " + JSON.stringify(id))
|
|
903
1213
|
}
|
|
1214
|
+
|
|
1215
|
+
const out: Record<string, any> = {}
|
|
1216
|
+
spec.parts.forEach((p: string, i: number) => { out[p] = got[i] })
|
|
1217
|
+
return out
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
// The id for a record the API returned.
|
|
1222
|
+
//
|
|
1223
|
+
// \`vals\` are the parameters THIS request addressed it with, and they win:
|
|
1224
|
+
// a response does not always repeat them. Otherwise the parts are read out
|
|
1225
|
+
// of the response through \`from\`, which is what makes a created or listed
|
|
1226
|
+
// record identifiable at all.
|
|
1227
|
+
//
|
|
1228
|
+
// THE ADDRESSING KEY WINS over an \`id\` the response already carries. A
|
|
1229
|
+
// response often has both — github's pull has a global database \`id\` and
|
|
1230
|
+
// a repo-scoped \`number\` — and the unrelated one is no use for addressing
|
|
1231
|
+
// anything. It is kept as \`${provider.lower}_id\` rather than dropped.
|
|
1232
|
+
function joinid(name: string, data: any, vals?: any) {
|
|
1233
|
+
const spec = ID_SPEC[name]
|
|
1234
|
+
if (null == data) {
|
|
1235
|
+
return data
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
let id = null
|
|
1239
|
+
|
|
1240
|
+
if (null != vals) {
|
|
1241
|
+
const got = spec.parts.map((p: string) => vals[p])
|
|
1242
|
+
if (got.every((v: any) => null != v && '' !== String(v))) {
|
|
1243
|
+
id = got.join(spec.sep)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (null == id) {
|
|
1248
|
+
const got = spec.parts.map((p: string) =>
|
|
1249
|
+
idread(data, (spec.from || {})[p] || p))
|
|
1250
|
+
if (got.every((v: any) =>
|
|
1251
|
+
null != v && 'object' !== typeof v && '' !== String(v))) {
|
|
1252
|
+
id = got.join(spec.sep)
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (null != id) {
|
|
1257
|
+
if (null != data.id && String(data.id) !== id &&
|
|
1258
|
+
null == ${jsProp('data', provider.lower + '_id')}) {
|
|
1259
|
+
${jsProp('data', provider.lower + '_id')} = data.id
|
|
1260
|
+
}
|
|
1261
|
+
data.id = id
|
|
1262
|
+
}
|
|
1263
|
+
|
|
904
1264
|
return data
|
|
905
1265
|
}
|
|
906
1266
|
|
|
907
1267
|
|
|
908
1268
|
`)
|
|
909
|
-
}
|
|
1269
|
+
}
|
|
910
1270
|
|
|
911
1271
|
// WHICH SDK OP SERVES EACH `action$`, per entity and per cmd.
|
|
912
1272
|
//
|
|
@@ -958,6 +1318,37 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
958
1318
|
// That is the silent drop in another hat: the caller named something the
|
|
959
1319
|
// entity does not have and was not told. An empty map inherits them all,
|
|
960
1320
|
// so a cmd with no actions was the most exposed.
|
|
1321
|
+
// THE CMD THAT WOULD WRITE TO THE WRONG RESOURCE.
|
|
1322
|
+
//
|
|
1323
|
+
// Some entities gather their ops from unrelated routes, and then a cmd's
|
|
1324
|
+
// only route addresses something that is not this record: \`migration\`'s
|
|
1325
|
+
// remove deletes a repository's migration ARCHIVE, \`user\`'s deletes a GPG
|
|
1326
|
+
// KEY, \`pull\`'s deletes a review COMMENT. The id the caller passed is not
|
|
1327
|
+
// in the request at all.
|
|
1328
|
+
//
|
|
1329
|
+
// Such a cmd is refused rather than sent. A caller asking to remove one
|
|
1330
|
+
// record must not have a different resource deleted instead, and a
|
|
1331
|
+
// successful-looking reply is the worst possible answer. Where the real
|
|
1332
|
+
// operation exists it is reachable by name, through \`action$\`.
|
|
1333
|
+
function misaddressed(
|
|
1334
|
+
entname: string, cmd: string, key: string, addresses: string[]
|
|
1335
|
+
) {
|
|
1336
|
+
const own = Object.prototype.hasOwnProperty
|
|
1337
|
+
const ents: any = own.call(ACTIONS, entname) ? ACTIONS[entname] : {}
|
|
1338
|
+
const acts = Object.keys(own.call(ents, cmd) ? ents[cmd] : {}).sort()
|
|
1339
|
+
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
'${provider.pkgName}: ' + entname + ' ' + cmd +
|
|
1342
|
+
': this API has no ' + cmd + ' route for one ' + entname +
|
|
1343
|
+
'. Its only ' + cmd + ' route addresses ' + addresses.join(', ') +
|
|
1344
|
+
', not ' + key + ', so the id would be ignored and a different record ' +
|
|
1345
|
+
'changed. ' +
|
|
1346
|
+
(0 < acts.length ?
|
|
1347
|
+
'Name the operation with action\$ instead: ' + acts.join(', ') :
|
|
1348
|
+
'No action\$ of this cmd is available either'))
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
|
|
961
1352
|
function actionop(name: string, entname: string, cmd: string) {
|
|
962
1353
|
const own = Object.prototype.hasOwnProperty
|
|
963
1354
|
const ents: any = own.call(ACTIONS, entname) ? ACTIONS[entname] : {}
|
|
@@ -1008,33 +1399,96 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1008
1399
|
// `save` guards the union of create's and update's keys: one action
|
|
1009
1400
|
// serves both and dispatches at runtime, so it cannot know which set
|
|
1010
1401
|
// applies until it has the data.
|
|
1402
|
+
// A COMPOSITE-KEY ENTITY GUARDS NOTHING SEPARATELY. Its parent keys
|
|
1403
|
+
// travel INSIDE the id, so demanding `q.owner` as well would reject
|
|
1404
|
+
// `load$('octocat/hello-world')` — the very call the composite id
|
|
1405
|
+
// exists to allow. splitid() does the checking instead, and refuses
|
|
1406
|
+
// a wrong part count by name.
|
|
1407
|
+
const eparts = idParts(e.ent)
|
|
1408
|
+
// The whole id description for this entity, or null when none is
|
|
1409
|
+
// needed. `out` branches on it rather than on the record key.
|
|
1410
|
+
const espec = idSpec(e.ent)
|
|
1011
1411
|
const guard = (cmd: string, src: string) => {
|
|
1012
1412
|
const keys = 'save' === cmd ?
|
|
1013
1413
|
[...new Set([...(e.opParents.create || []), ...(e.opParents.update || [])])].sort() :
|
|
1014
1414
|
(e.opParents[cmd] || [])
|
|
1015
1415
|
|
|
1016
1416
|
return keys
|
|
1417
|
+
.filter((k: string) => !eparts.includes(k))
|
|
1017
1418
|
.map((k: string) =>
|
|
1018
1419
|
` ${guardName(e, k)}(${jsProp(src, k)}, '${cmd}')\n`)
|
|
1019
1420
|
.join('')
|
|
1020
1421
|
}
|
|
1021
1422
|
|
|
1423
|
+
// THE REFUSAL LINE, for a cmd whose only route addresses a
|
|
1424
|
+
// different resource. Placed AFTER the action branch — an action
|
|
1425
|
+
// route is named explicitly and is exactly how the real operation is
|
|
1426
|
+
// reached — and BEFORE the guards, so the caller is told the cmd
|
|
1427
|
+
// does not exist for this entity rather than being asked for a
|
|
1428
|
+
// parameter that would not have helped.
|
|
1429
|
+
const refuse = (cmd: string) => {
|
|
1430
|
+
if (true !== (e.idmisaddressed || {})[cmd]) {
|
|
1431
|
+
return ''
|
|
1432
|
+
}
|
|
1433
|
+
const key = 0 < eparts.length ? eparts.join(String(e.idsep || '/')) :
|
|
1434
|
+
recordKey(e.ent)
|
|
1435
|
+
const addresses = addressKeys(e.ent, cmd)
|
|
1436
|
+
|
|
1437
|
+
return ` misaddressed('${e.name}', '${cmd}', '${key}', ` +
|
|
1438
|
+
`[${addresses.map((k: string) => `'${k}'`).join(', ')}])
|
|
1439
|
+
`
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1022
1442
|
// Reading the record's own key off the Seneca query, which always
|
|
1023
1443
|
// spells it `id`, and every other required key off its own name.
|
|
1024
1444
|
const rk = recordKey(e.ent)
|
|
1445
|
+
|
|
1446
|
+
// For a composite key the parameters come out of the split, which the
|
|
1447
|
+
// handler binds to `key` before the call. Any required key the id
|
|
1448
|
+
// does NOT carry still comes off the query.
|
|
1449
|
+
// THE RECORD'S OWN KEY IS ALWAYS SENT, whether or not the match
|
|
1450
|
+
// declares it required.
|
|
1451
|
+
//
|
|
1452
|
+
// An op gathers several routes, and a parameter only one of them
|
|
1453
|
+
// uses comes through OPTIONAL: github's IssueLoadMatch has `owner`
|
|
1454
|
+
// and `repo` required but `id` optional, because the op also covers
|
|
1455
|
+
// `/repos/{owner}/{repo}/issues/comments/{comment_id}`. Sending only
|
|
1456
|
+
// the required keys called `Issue().load({owner, repo})` — the id
|
|
1457
|
+
// the caller passed to `load$` went nowhere, so every read of any
|
|
1458
|
+
// issue in that repo answered with the same record and
|
|
1459
|
+
// `load$('no-such-issue')` returned one. Ten entities failed their
|
|
1460
|
+
// not-found test on it, and the ones that "passed" were passing for
|
|
1461
|
+
// the wrong reason. addressKeys is what the test emitter reads too.
|
|
1025
1462
|
const sdkArg = (opname: string) => {
|
|
1026
|
-
const keys =
|
|
1463
|
+
const keys = addressKeys(e.ent, opname)
|
|
1027
1464
|
if (0 === keys.length) {
|
|
1028
1465
|
return '{}'
|
|
1029
1466
|
}
|
|
1467
|
+
if (0 < eparts.length) {
|
|
1468
|
+
return `{ ${keys.map((k: string) => eparts.includes(k) ?
|
|
1469
|
+
`${jsKey(k)}: ${jsProp('key', k)}` :
|
|
1470
|
+
`${jsKey(k)}: ${jsProp('q', k)}`).join(', ')} }`
|
|
1471
|
+
}
|
|
1030
1472
|
return `{ ${keys.map((k: string) =>
|
|
1031
1473
|
`${jsKey(k)}: ${jsProp('q', k === rk ? 'id' : k)}`).join(', ')} }`
|
|
1032
1474
|
}
|
|
1033
1475
|
|
|
1476
|
+
// The line that splits the Seneca id into the API's parameters,
|
|
1477
|
+
// emitted only where there is one record to address. `list` has none.
|
|
1478
|
+
// The entity-options argument that carries the path parameters for a
|
|
1479
|
+
// write. Empty for an ordinary entity, which needs no such channel.
|
|
1480
|
+
const entArg = 0 === eparts.length ? '' :
|
|
1481
|
+
`null == key ? undefined : { match: key }`
|
|
1482
|
+
|
|
1483
|
+
const splitLine = (cmd: string) => 0 === eparts.length ? '' :
|
|
1484
|
+
` const key = splitid('${e.name}', ${'save' === cmd ? 'data.id' : 'q.id'}, '${cmd}')\n`
|
|
1485
|
+
|
|
1034
1486
|
// The data hop, plus the id alias when the API keys the record by
|
|
1035
|
-
// something other than `id`.
|
|
1036
|
-
|
|
1037
|
-
|
|
1487
|
+
// something other than `id`. A composite entity passes the addressing
|
|
1488
|
+
// values through, because the response may not repeat them.
|
|
1489
|
+
const out = (expr: string, vals?: string) =>
|
|
1490
|
+
null == espec ? `plain(${expr})` :
|
|
1491
|
+
`joinid('${e.name}', plain(${expr})${null == vals ? '' : ', ' + vals})`
|
|
1038
1492
|
|
|
1039
1493
|
// The action branch, emitted for every cmd whether or not this entity
|
|
1040
1494
|
// has actions. `actionop` is what refuses an unknown name, so leaving
|
|
@@ -1084,8 +1538,8 @@ ${actionBranch('list',
|
|
|
1084
1538
|
${actionBranch('load',
|
|
1085
1539
|
` const hit = await ornull(() => this.shared.sdk.${e.acc}()[op$](actionq(msg.q, '${rk}', action$)))
|
|
1086
1540
|
return null == hit ? null : entize(${out('hit')})
|
|
1087
|
-
`)}${guard('load', 'q')} const res = await ornull(() => this.shared.sdk.${e.acc}().load(${sdkArg('load')}))
|
|
1088
|
-
return null == res ? null : entize(${out('res')})
|
|
1541
|
+
`)}${guard('load', 'q')}${splitLine('load')} const res = await ornull(() => this.shared.sdk.${e.acc}().load(${sdkArg('load')}))
|
|
1542
|
+
return null == res ? null : entize(${out('res', 0 < eparts.length ? 'key' : undefined)})
|
|
1089
1543
|
}
|
|
1090
1544
|
|
|
1091
1545
|
`)
|
|
@@ -1099,22 +1553,94 @@ ${actionBranch('load',
|
|
|
1099
1553
|
// entity's data, so its id lives at `id` whatever the API calls it —
|
|
1100
1554
|
// dispatching on the API's key sent every save to `create`, leaving
|
|
1101
1555
|
// update unreachable.
|
|
1556
|
+
// A MISADDRESSED UPDATE IS REFUSED, BUT ONLY ON THE UPDATE LEG. A
|
|
1557
|
+
// create needs no record key — the API assigns one — so an entity
|
|
1558
|
+
// whose update route addresses a different resource can still be
|
|
1559
|
+
// created. Dispatch is on `data.id`, so the refusal goes exactly
|
|
1560
|
+
// where the update would have.
|
|
1561
|
+
const refuseUpdate = true === (e.idmisaddressed || {}).update ?
|
|
1562
|
+
refuse('update') : ''
|
|
1563
|
+
|
|
1102
1564
|
const body = hasCreate && hasUpdate
|
|
1103
|
-
? ` const res = null == data.id
|
|
1104
|
-
? await sdk.${e.acc}().create(data)
|
|
1105
|
-
: await sdk.${e.acc}().update(data)`
|
|
1565
|
+
? (('' === refuseUpdate) ? ` const res = null == data.id
|
|
1566
|
+
? await sdk.${e.acc}(${entArg}).create(data)
|
|
1567
|
+
: await sdk.${e.acc}(${entArg}).update(data)` : ` if (null != data.id) {
|
|
1568
|
+
${refuseUpdate.replace(/\n$/, '')}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const res = await sdk.${e.acc}(${entArg}).create(data)`)
|
|
1106
1572
|
: hasCreate
|
|
1107
|
-
? ` const res = await sdk.${e.acc}().create(data)`
|
|
1108
|
-
:
|
|
1573
|
+
? ` const res = await sdk.${e.acc}(${entArg}).create(data)`
|
|
1574
|
+
: `${refuseUpdate} const res = await sdk.${e.acc}(${entArg}).update(data)`
|
|
1109
1575
|
|
|
1110
1576
|
// ... and hand the API back its own key, which Seneca does not know
|
|
1111
1577
|
// to send.
|
|
1112
|
-
|
|
1578
|
+
//
|
|
1579
|
+
// A COMPOSITE KEY IS UNPACKED ONTO THE DATA. The write goes out as
|
|
1580
|
+
// a body plus path parameters, and the SDK reads those parameters
|
|
1581
|
+
// off the same object — so the parts have to be present under
|
|
1582
|
+
// their own names, not fused into `id`. On a create there is no id
|
|
1583
|
+
// yet and the caller supplies the parts directly, which is why this
|
|
1584
|
+
// only runs when an id is there.
|
|
1585
|
+
// THE COMPOSITE SPLIT CANNOT LIVE HERE, before the action branch,
|
|
1586
|
+
// even though that is where the single-key alias sits. The alias
|
|
1587
|
+
// only ever assigns; the split THROWS on an id that is not all its
|
|
1588
|
+
// parts, and an `action$` call is entitled to an id shaped however
|
|
1589
|
+
// that action's own route wants. Running it first turned
|
|
1590
|
+
// `action$: 'no_such_action'` into an id complaint, hiding the
|
|
1591
|
+
// error the caller needed. It is emitted after the action branch
|
|
1592
|
+
// instead — see compositeSave below, spliced where the parent
|
|
1593
|
+
// guards go, which is exactly the position the component already
|
|
1594
|
+
// documents as "after the action branch".
|
|
1595
|
+
const compositeSave = 0 < eparts.length ? `
|
|
1596
|
+
// Seneca carries this ${e.name}'s key as one \`id\`; the API addresses
|
|
1597
|
+
// the record by ${eparts.map((p: string) => '`' + p + '`').join(' and ')}.
|
|
1598
|
+
//
|
|
1599
|
+
// THE PARTS GO IN THE ENTITY MATCH, NOT ONTO THE DATA. They are path
|
|
1600
|
+
// parameters, and the data is the request body. Writing them onto the
|
|
1601
|
+
// data is how the flat \`${eparts[0]}\` the URL needs came to displace
|
|
1602
|
+
// whatever the response carries under that name — for github's repo an
|
|
1603
|
+
// \`owner\` OBJECT, so a saved record lost the field that identifies
|
|
1604
|
+
// it. The SDK resolves a path parameter from the match ahead of the
|
|
1605
|
+
// body, so passing it here leaves the body exactly as the caller meant
|
|
1606
|
+
// it.
|
|
1607
|
+
//
|
|
1608
|
+
// \`key\` STAYS NULL ON A CREATE: there is no id yet, the API assigns
|
|
1609
|
+
// the record, and the id is rebuilt from the response instead.
|
|
1610
|
+
let key = null
|
|
1611
|
+
if (null != data.id) {
|
|
1612
|
+
key = splitid('${e.name}', data.id, 'save')
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// \`${provider.lower}_id\` is this provider's own bookkeeping — the
|
|
1616
|
+
// API's unrelated \`id\`, parked by joinid() so it is not lost. It is
|
|
1617
|
+
// not a field of the API's write schema, so it must not travel in the
|
|
1618
|
+
// request body.
|
|
1619
|
+
delete ${jsProp('data', provider.lower + '_id')}
|
|
1620
|
+
|
|
1621
|
+
// AND NEITHER DOES THE JOINED \`id\`. It is Seneca's key for this
|
|
1622
|
+
// record, not the API's: a composite ${e.name} is addressed by
|
|
1623
|
+
// \`${eparts.join('\` and \`')}\`, which travel as path parameters in
|
|
1624
|
+
// the match above. Leaving it on the body sent \`owner0/repo0\` as a
|
|
1625
|
+
// field the write schema has no place for — and the offline transport,
|
|
1626
|
+
// which matches a request against a stored record, then looked for a
|
|
1627
|
+
// record whose own \`id\` was that joined string and found none.
|
|
1628
|
+
delete data.id
|
|
1629
|
+
` : ''
|
|
1630
|
+
|
|
1631
|
+
const alias = 0 < eparts.length ? '' : 'id' === rk ? '' :
|
|
1113
1632
|
`
|
|
1114
1633
|
// This API keys a ${e.name} by \`${rk}\`; Seneca carries it as \`id\`.
|
|
1115
1634
|
if (null == ${jsProp('data', rk)} && null != data.id) {
|
|
1116
1635
|
${jsProp('data', rk)} = data.id
|
|
1117
1636
|
}
|
|
1637
|
+
|
|
1638
|
+
// \`${provider.lower}_id\` is this provider's own bookkeeping — the
|
|
1639
|
+
// API's unrelated \`id\`, parked by joinid() so it is not lost. It
|
|
1640
|
+
// is not a field of the API's write schema, so it must not travel in
|
|
1641
|
+
// the request body: a strict API rejects an unknown property, and a
|
|
1642
|
+
// lax one may persist it.
|
|
1643
|
+
delete ${jsProp('data', provider.lower + '_id')}
|
|
1118
1644
|
`
|
|
1119
1645
|
|
|
1120
1646
|
Content(`
|
|
@@ -1130,9 +1656,9 @@ ${actionBranch('save',
|
|
|
1130
1656
|
data.$action = action$
|
|
1131
1657
|
const done = await sdk.${e.acc}()[op$](data)
|
|
1132
1658
|
return entize(${out('done')})
|
|
1133
|
-
`)}${guard('save', 'data')}${body}
|
|
1659
|
+
`)}${guard('save', 'data')}${compositeSave}${body}
|
|
1134
1660
|
|
|
1135
|
-
return entize(${out('res')})
|
|
1661
|
+
return entize(${out('res', 0 < eparts.length ? 'key' : undefined)})
|
|
1136
1662
|
}
|
|
1137
1663
|
|
|
1138
1664
|
`)
|
|
@@ -1160,8 +1686,9 @@ ${actionBranch('save',
|
|
|
1160
1686
|
${actionBranch('remove',
|
|
1161
1687
|
` const gone = await ornull(() => this.shared.sdk.${e.acc}()[op$](actionq(msg.q, '${rk}', action$)))
|
|
1162
1688
|
return null == gone ? null : entize(${out('gone')})
|
|
1163
|
-
`)}${
|
|
1164
|
-
|
|
1689
|
+
`)}${'' !== refuse('remove') ? refuse('remove') :
|
|
1690
|
+
`${guard('remove', 'q')}${splitLine('remove')} await ornull(() => this.shared.sdk.${e.acc}().remove(${sdkArg('remove')}))
|
|
1691
|
+
`} return null
|
|
1165
1692
|
}
|
|
1166
1693
|
|
|
1167
1694
|
`)
|
|
@@ -1181,9 +1708,20 @@ ${actionBranch('remove',
|
|
|
1181
1708
|
const sdkopts: any = Object.assign({}, options.sdk)
|
|
1182
1709
|
${provider.authActive ? `
|
|
1183
1710
|
// The provider convention carries credentials, so honour an \`apikey\`
|
|
1184
|
-
// when one is configured
|
|
1711
|
+
// when one is configured.
|
|
1185
1712
|
const res = await this.post('sys:provider,get:keymap,provider:${provider.lower}')
|
|
1186
|
-
|
|
1713
|
+
|
|
1714
|
+
// ACCEPT \`api\` AS WELL AS \`apikey\`. The older provider convention
|
|
1715
|
+
// named this key \`api\` and read it with
|
|
1716
|
+
// \`sys:provider,get:key,...,key:api\`; the keymap message replaced that,
|
|
1717
|
+
// and the rename was silent. An application still configured as
|
|
1718
|
+
// \`keys: { api: { value: ... } }\` therefore resolved to undefined and
|
|
1719
|
+
// the SDK was constructed with NO credential at all — the request went
|
|
1720
|
+
// out unauthenticated and failed much later as a 401 or a 404 on
|
|
1721
|
+
// anything private, with nothing at startup to point at the cause.
|
|
1722
|
+
// \`apikey\` wins when both are set, so a config that has migrated is
|
|
1723
|
+
// unaffected.
|
|
1724
|
+
const apikey = res?.keymap?.apikey?.value ?? res?.keymap?.api?.value
|
|
1187
1725
|
|
|
1188
1726
|
// Hand the credential to the SDK as \`apikey\`, NOT as an authorization
|
|
1189
1727
|
// HEADER. The SDK's own auth stage owns that header: it reads
|
|
@@ -1196,6 +1734,23 @@ ${provider.authActive ? `
|
|
|
1196
1734
|
if (null != apikey && '' !== apikey) {
|
|
1197
1735
|
sdkopts.apikey = apikey
|
|
1198
1736
|
}
|
|
1737
|
+
|
|
1738
|
+
// AN UNRESOLVED CREDENTIAL IS SAID OUT LOUD. This API declares
|
|
1739
|
+
// authentication, so reaching here with nothing configured means every
|
|
1740
|
+
// call goes out unauthenticated. That is not always wrong — public
|
|
1741
|
+
// read-only endpoints work, at a much lower rate limit — so this warns
|
|
1742
|
+
// rather than throwing, and names both accepted key spellings so a
|
|
1743
|
+
// misnamed key is obvious from one line of log. Silence here is what
|
|
1744
|
+
// made the \`api\` -> \`apikey\` rename above cost a debugging session
|
|
1745
|
+
// instead of a glance.
|
|
1746
|
+
else {
|
|
1747
|
+
this.log.warn({
|
|
1748
|
+
fix: 'unauthenticated',
|
|
1749
|
+
note: 'no ${provider.lower} credential resolved from the keymap ' +
|
|
1750
|
+
'(looked for keys.apikey then keys.api); requests will be sent ' +
|
|
1751
|
+
'unauthenticated and will fail on anything non-public',
|
|
1752
|
+
})
|
|
1753
|
+
}
|
|
1199
1754
|
${provider.authBasic ? `
|
|
1200
1755
|
// Genuine HTTP Basic Auth needs a SECOND credential (the SDK sends
|
|
1201
1756
|
// \`Authorization: Basic base64(apikey:secret)\`) — without it the SDK's
|