@voxgig/sdkgen-infrapack 0.0.1
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 +120 -0
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +4362 -0
- package/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +161 -0
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +1295 -0
- package/.sdk/tm/seneca-provider/CODE_OF_CONDUCT.md +132 -0
- package/.sdk/tm/seneca-provider/LICENSE +21 -0
- package/.sdk/tm/seneca-provider/Makefile +15 -0
- package/.sdk/tm/seneca-provider/src/tsconfig.json +18 -0
- package/.sdk/tm/seneca-provider/test/tsconfig.json +16 -0
- package/.sdk/tm/seneca-provider/tsfmt.json +3 -0
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/package.json +98 -0
- package/sdkgen-package.json +18 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cmp, each,
|
|
3
|
+
File, Content, Copy, Folder,
|
|
4
|
+
entityCollection, entityOps, entityIdField, entityClassName,
|
|
5
|
+
entityActions,
|
|
6
|
+
opRequestShape, opParams, ownPoint, entityPath,
|
|
7
|
+
collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
|
|
8
|
+
authorInfo, contributorList, isAuthActive, isHttpBasicAuth, jsKey, jsProp,
|
|
9
|
+
SdkGenError,
|
|
10
|
+
PUBLISHER, PUBLISHER_URL,
|
|
11
|
+
pointSegments,
|
|
12
|
+
} from '@voxgig/sdkgen'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
KIT,
|
|
16
|
+
} from '@voxgig/apidef'
|
|
17
|
+
|
|
18
|
+
import { Tests, Scripts, Workflow, Readme, Docs } from './Extras_seneca-provider'
|
|
19
|
+
import { Gitignore } from './Gitignore_seneca-provider'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// The `seneca-provider` target: a Seneca plugin exposing this API's entities
|
|
23
|
+
// as Seneca entities (`provider/<name>/<entity>`), layered on the sibling
|
|
24
|
+
// `ts` SDK.
|
|
25
|
+
//
|
|
26
|
+
// A consumer target in the go-cli / py-data mould — every standard phase is
|
|
27
|
+
// off in model/target/seneca-provider.aon and this component emits the
|
|
28
|
+
// whole package. It differs from those in one way that shapes everything
|
|
29
|
+
// here: it generates into ITS OWN REPO (`output: path`), depends on the SDK
|
|
30
|
+
// as a PUBLISHED npm package rather than by path, and therefore carries a
|
|
31
|
+
// repo's worth of furniture rather than a subfolder's.
|
|
32
|
+
//
|
|
33
|
+
// SHAPE OF THE MAPPING
|
|
34
|
+
//
|
|
35
|
+
// Seneca's store commands are list / load / save / remove. The SDK's are
|
|
36
|
+
// list / load / create / update / remove. `save` is the one that is not
|
|
37
|
+
// one-to-one: Seneca's convention is that an entity carrying an id is an
|
|
38
|
+
// update and one without is a create, so `save` dispatches on `data.id`.
|
|
39
|
+
//
|
|
40
|
+
// Everything else follows from the model:
|
|
41
|
+
// - which cmds exist at all, from the entity's declared ops;
|
|
42
|
+
// - the required path params of each op, which become argument guards (a
|
|
43
|
+
// nested entity like `moon` under `/planet/{planet_id}/moon` cannot build
|
|
44
|
+
// its URL without the parent id, and an opaque 404 from a half-built URL
|
|
45
|
+
// is a bad error message);
|
|
46
|
+
// - the SDK accessor and entity class names, from the same helpers the ts
|
|
47
|
+
// target uses, so the two cannot drift.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// Seneca store cmd -> the SDK ops it needs. `save` needs BOTH create and
|
|
51
|
+
// update; it is emitted when either is present and dispatches on the id.
|
|
52
|
+
const CMD_OPS: Record<string, string[]> = {
|
|
53
|
+
list: ['list'],
|
|
54
|
+
load: ['load'],
|
|
55
|
+
save: ['create', 'update'],
|
|
56
|
+
remove: ['remove'],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// The custom ACTIONS one Seneca cmd can reach, as action name -> SDK op.
|
|
61
|
+
//
|
|
62
|
+
// apidef folds a non-CRUD verb into an ordinary op as an extra point marked
|
|
63
|
+
// `select.$action`: GitHub's `PUT /repos/{owner}/{repo}/pulls/{n}/merge` is a
|
|
64
|
+
// second point of `pull.update`, beside the canonical `PATCH`. The SDK
|
|
65
|
+
// selects one with `$action` in the call's argument; without this map the
|
|
66
|
+
// provider has no way to name one at all, and `merge` is simply unreachable
|
|
67
|
+
// through a generated plugin.
|
|
68
|
+
//
|
|
69
|
+
// KEYED BY CMD, NOT BY OP, and that is the whole point of the function.
|
|
70
|
+
// `save` covers create AND update, so an action folded into `create` arrives
|
|
71
|
+
// through `save$` exactly as one folded into `update` does — assuming
|
|
72
|
+
// `update` would send `upload_image` (petstore's
|
|
73
|
+
// `POST /pet/{petId}/uploadImage`, a create point) to the wrong endpoint, and
|
|
74
|
+
// the SDK would then refuse it as an invalid action on an operation the
|
|
75
|
+
// caller never named.
|
|
76
|
+
//
|
|
77
|
+
// A name claimed by an earlier op WINS: `entityActions` walks the op map in
|
|
78
|
+
// sorted-key order, so for `save` that is create before update. Two ops of
|
|
79
|
+
// one entity sharing an action name is not something apidef produces from a
|
|
80
|
+
// spec — the name comes from the route — and if it ever does, a stable choice
|
|
81
|
+
// beats a last-writer-wins one.
|
|
82
|
+
function cmdActions(ent: any, cmd: string): Record<string, string> {
|
|
83
|
+
const ops = CMD_OPS[cmd] || []
|
|
84
|
+
|
|
85
|
+
// ACTIVE ops only, for the same reason `parentKeys` uses `entityOps`: an op
|
|
86
|
+
// the model marks `active: false` generates no SDK method, so an action
|
|
87
|
+
// folded into it is not reachable and must not be advertised as if it were.
|
|
88
|
+
const live = entityOps(ent)
|
|
89
|
+
const out: Record<string, string> = {}
|
|
90
|
+
|
|
91
|
+
// `null == out[a.action]` on a plain object reads Object's PROTOTYPE for a
|
|
92
|
+
// name like `toString` or `constructor`, finds a function, and concludes
|
|
93
|
+
// the name is already claimed — dropping a modelled action that happens to
|
|
94
|
+
// carry one. Ask whether the map itself has the key.
|
|
95
|
+
const claimed = (name: string) =>
|
|
96
|
+
Object.prototype.hasOwnProperty.call(out, name)
|
|
97
|
+
|
|
98
|
+
for (const a of entityActions(ent)) {
|
|
99
|
+
if (ops.includes(a.op) && live.includes(a.op) && !claimed(a.action)) {
|
|
100
|
+
out[a.action] = a.op
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return out
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// The ops that have a route of their OWN, as opposed to nothing but folded-in
|
|
109
|
+
// actions. An op whose every point is an action point has no plain call: a
|
|
110
|
+
// `save$` naming no action still reaches the SDK's `update`, but the SDK finds
|
|
111
|
+
// one point and takes it, so the "canonical" update IS the action's route.
|
|
112
|
+
//
|
|
113
|
+
// Which is what the generated tests need to know before writing a plain,
|
|
114
|
+
// id-bearing save: without this they were emitted for an entity that has no
|
|
115
|
+
// such call to make.
|
|
116
|
+
function canonicalOps(ent: any): string[] {
|
|
117
|
+
return entityOps(ent).filter((opname: string) => {
|
|
118
|
+
const op = (ent.op || {})[opname]
|
|
119
|
+
const points: any[] = (op && op.points) || []
|
|
120
|
+
|
|
121
|
+
return points.some((pt: any) =>
|
|
122
|
+
null == (pt && pt.select && pt.select['$action']))
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// Every action the entity exposes, with the cmd that reaches it — for the
|
|
128
|
+
// README and the generated tests, which describe the entity rather than one
|
|
129
|
+
// call. `cmd` is what a Seneca user types; `op` is what the SDK is asked for.
|
|
130
|
+
function entityActionList(ent: any):
|
|
131
|
+
{ cmd: string, op: string, action: string, path: string }[] {
|
|
132
|
+
const out: { cmd: string, op: string, action: string, path: string }[] = []
|
|
133
|
+
|
|
134
|
+
for (const cmd of Object.keys(CMD_OPS).sort()) {
|
|
135
|
+
const map = cmdActions(ent, cmd)
|
|
136
|
+
for (const a of entityActions(ent)) {
|
|
137
|
+
if (map[a.action] === a.op) {
|
|
138
|
+
out.push({ cmd, op: a.op, action: a.action, path: a.path })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
// The required (non-optional) request keys of an op, id first. These are what
|
|
148
|
+
// the SDK needs to build the path, so they are what the provider must have
|
|
149
|
+
// before it calls.
|
|
150
|
+
function requiredKeys(ent: any, opname: string): string[] {
|
|
151
|
+
const idf = entityIdField(ent)
|
|
152
|
+
return opRequestShape(ent, opname).items
|
|
153
|
+
.filter((it: any) => !it.optional)
|
|
154
|
+
.map((it: any) => it.name)
|
|
155
|
+
.sort((a: string, b: string) => (a === idf ? 0 : 1) - (b === idf ? 0 : 1))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// The key that addresses ONE record.
|
|
160
|
+
//
|
|
161
|
+
// `entityIdField` answers whenever the model declares one, which apidef does
|
|
162
|
+
// for any entity carrying a field literally named `id` — and it renames an
|
|
163
|
+
// `<entity>_id` path param to `id` besides, which is why most APIs never reach
|
|
164
|
+
// the fallback.
|
|
165
|
+
//
|
|
166
|
+
// When it does NOT answer, the record key is the LAST path param of the
|
|
167
|
+
// op's own point, in PATH order — a route addresses parents first and the
|
|
168
|
+
// record last, by construction. Without this the key was simply unknown,
|
|
169
|
+
// so a param named `code` failed the `!== idf` test in opParentKeys and
|
|
170
|
+
// was classified as a PARENT — `load$('SAVE20')` threw "coupon load: code
|
|
171
|
+
// is required" instead of loading anything, and the entity was treated as
|
|
172
|
+
// nested throughout.
|
|
173
|
+
//
|
|
174
|
+
// opParams(op) is NOT the source here: it alphabetizes params for output
|
|
175
|
+
// stability, which loses path order on a 3+-param route — Airtable's
|
|
176
|
+
// record (base_id, table_id, record_id) alphabetizes with table_id last,
|
|
177
|
+
// so the old `params[params.length - 1]` picked the wrong parent as the
|
|
178
|
+
// record's own key. The point's own `parts` still has the true order.
|
|
179
|
+
function recordKey(ent: any): string {
|
|
180
|
+
const idf = entityIdField(ent)
|
|
181
|
+
if (null != idf && '' !== idf) {
|
|
182
|
+
return String(idf)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const opname of ['load', 'remove', 'update']) {
|
|
186
|
+
const op = (ent.op || {})[opname]
|
|
187
|
+
if (null == op || 0 === (op.points || []).length) {
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const canonical = op.points.filter((pt: any) =>
|
|
192
|
+
null == (pt && pt.select && pt.select['$action']))
|
|
193
|
+
const point = ownPoint(0 < canonical.length ? canonical : op.points)
|
|
194
|
+
// The LAST variable segment names the record's key. apidef states which
|
|
195
|
+
// segments are variables (its ADR-003), so this reads the name off the
|
|
196
|
+
// vector rather than finding a `{` and slicing the braces back off.
|
|
197
|
+
const vars = pointSegments(point)
|
|
198
|
+
.filter((seg: any) => null != seg.var)
|
|
199
|
+
|
|
200
|
+
if (0 < vars.length) {
|
|
201
|
+
return String(vars[vars.length - 1].var)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// No path param at all (e.g. GET /scan/async/result?trace_id=...): the
|
|
205
|
+
// record's own key can still be a single required QUERY param.
|
|
206
|
+
const query = (point && point.args && point.args.query) || []
|
|
207
|
+
const reqdQuery = query.filter((q: any) => false !== q.reqd)
|
|
208
|
+
if (1 === reqdQuery.length) {
|
|
209
|
+
return String(reqdQuery[0].name)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return 'id'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
// The guard function's name. Spec-derived param names are not constrained to
|
|
218
|
+
// identifiers — Evervault's `/payments/3ds-sessions/{3ds_session_id}` is the
|
|
219
|
+
// standing example — and a name is a DECLARATION here, so it cannot be
|
|
220
|
+
// bracket-quoted the way a property access can. Non-identifier characters are
|
|
221
|
+
// replaced rather than dropped, so `a-b` and `a_b` cannot collide.
|
|
222
|
+
function guardName(e: any, key: string): string {
|
|
223
|
+
return `need_${e.name}_${String(key).replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
224
|
+
.replace(/^need_(\d)/, 'need__$1')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
// The parent PATH params of ONE op: `moon`'s load under
|
|
229
|
+
// `/planet/{planet_id}/moon/{id}` yields ['planet_id']. These are the keys a
|
|
230
|
+
// caller can forget, so each gets a guard.
|
|
231
|
+
//
|
|
232
|
+
// From opParams — the op's declared path params — NOT from opRequestShape.
|
|
233
|
+
// For create/update the latter also returns the request BODY fields, so
|
|
234
|
+
// reading it here generated a "planet name is required" guard for every
|
|
235
|
+
// writable field on every entity.
|
|
236
|
+
function opParentKeys(ent: any, opname: string): string[] {
|
|
237
|
+
const rk = recordKey(ent)
|
|
238
|
+
const op = (ent.op || {})[opname]
|
|
239
|
+
|
|
240
|
+
if (null == op) {
|
|
241
|
+
return []
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const seen = new Set<string>()
|
|
245
|
+
|
|
246
|
+
for (const p of opParams(op)) {
|
|
247
|
+
const name = String((p as any).name)
|
|
248
|
+
if (false !== (p as any).reqd && name !== rk && name !== 'id') {
|
|
249
|
+
seen.add(name)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [...seen].sort()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// Every parent key the entity has, across its ACTIVE ops — for the seed data
|
|
258
|
+
// and the docs, which describe the entity rather than one call.
|
|
259
|
+
//
|
|
260
|
+
// `entityOps` and not `Object.keys(ent.op)`: an op the model marks
|
|
261
|
+
// `active: false` generates no SDK method, so letting it contribute a key
|
|
262
|
+
// here put a mandatory guard for a parameter of a call that does not exist
|
|
263
|
+
// onto every cmd that does.
|
|
264
|
+
function parentKeys(ent: any): string[] {
|
|
265
|
+
const seen = new Set<string>()
|
|
266
|
+
|
|
267
|
+
for (const opname of entityOps(ent)) {
|
|
268
|
+
for (const key of opParentKeys(ent, opname)) {
|
|
269
|
+
seen.add(key)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return [...seen].sort()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
// A model field's broad shape, for generating seed data that reads as data.
|
|
278
|
+
// The model carries canon strings (`\`$STRING\``), not JS types.
|
|
279
|
+
//
|
|
280
|
+
// ORDER MATTERS and the tests are substring tests, so the container kinds are
|
|
281
|
+
// checked FIRST: a multi-type field's sentinel is the ARRAY
|
|
282
|
+
// `['`$ONE`', [members...]]`, and String() flattens it to a comma-joined
|
|
283
|
+
// string — so a `$ONE` of string|number matched `includes('NUMBER')` and was
|
|
284
|
+
// seeded as a bare number. A union is not a number; it is whatever its first
|
|
285
|
+
// member is, and falling back to a string is the safe answer.
|
|
286
|
+
//
|
|
287
|
+
// `$ARRAY` and `$OBJECT` are in the sentinel vocabulary (see
|
|
288
|
+
// helpers/canonType.ts) and used to fall through to 'string', which put
|
|
289
|
+
// `tags: 'quick-tags'` into test/quick.js — a type-incorrect body that a
|
|
290
|
+
// validating server rejects.
|
|
291
|
+
function fieldKind(type: any): string {
|
|
292
|
+
if (Array.isArray(type)) {
|
|
293
|
+
return 'string'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const t = String(type || '').toUpperCase()
|
|
297
|
+
|
|
298
|
+
if (t.includes('ARRAY') || t.includes('LIST')) return 'array'
|
|
299
|
+
if (t.includes('OBJECT') || t.includes('MAP')) return 'object'
|
|
300
|
+
if (t.includes('BOOLEAN')) return 'boolean'
|
|
301
|
+
if (t.includes('NUMBER') || t.includes('INTEGER')) return 'number'
|
|
302
|
+
|
|
303
|
+
return 'string'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
// Which entity a parent path param refers to: `planet_id` -> `planet`, but
|
|
308
|
+
// only when an entity of that name actually exists. A key that names no
|
|
309
|
+
// entity gets no cross-reference, and the seed falls back to a plain string.
|
|
310
|
+
function parentEntityOf(key: string, names: string[]): string {
|
|
311
|
+
const stem = key.replace(/_id$/, '')
|
|
312
|
+
return names.includes(stem) ? stem : ''
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
// The repo this provider is released from — NOT the SDK's. A provider is its
|
|
317
|
+
// own package in its own repo, so its manifest's homepage/repository must
|
|
318
|
+
// point there; deriving them from `main: kit: repo` sends every link in the
|
|
319
|
+
// published package to the SDK instead.
|
|
320
|
+
//
|
|
321
|
+
// Order: the project's `output: repo`, else the Seneca convention.
|
|
322
|
+
function providerRepo(model: any, lower: string, tname: string):
|
|
323
|
+
{ url: string, path: string } {
|
|
324
|
+
const host = model?.main?.[KIT]?.repo?.host || 'github.com'
|
|
325
|
+
const declared = model?.main?.[KIT]?.target?.[tname]?.output?.repo
|
|
326
|
+
const path = null != declared && '' !== declared ?
|
|
327
|
+
String(declared) : `senecajs/seneca-${lower}-provider`
|
|
328
|
+
|
|
329
|
+
return { url: `https://${host}/${path}`, path }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
// The provider's published package name: the project's pin when it has one,
|
|
334
|
+
// else `@seneca/<name>-provider`. packageName() cannot answer this — its
|
|
335
|
+
// derivations are all SDK-shaped (`@<origin>/<slug>-sdk`).
|
|
336
|
+
function providerPackage(model: any, lower: string, tname: string): string {
|
|
337
|
+
const declared = model?.main?.[KIT]?.target?.[tname]
|
|
338
|
+
?.publish?.registry?.package
|
|
339
|
+
return null != declared && '' !== declared ?
|
|
340
|
+
String(declared) : `@seneca/${lower}-provider`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
const Main = cmp(function Main(props: any) {
|
|
345
|
+
const { target, ctx$ } = props
|
|
346
|
+
const { model } = ctx$
|
|
347
|
+
|
|
348
|
+
// HARD REQUIREMENT: this plugin imports the TypeScript SDK. Generating it
|
|
349
|
+
// without `ts` produces a package whose every import fails, so fail at
|
|
350
|
+
// GENERATE time with an actionable message instead.
|
|
351
|
+
const targets = model.main[KIT].target || {}
|
|
352
|
+
if (null == targets.ts) {
|
|
353
|
+
throw new SdkGenError(
|
|
354
|
+
'seneca-provider requires the `ts` target in the same SDK: it imports ' +
|
|
355
|
+
'the TypeScript SDK that `ts` generates. Add it with:\n' +
|
|
356
|
+
' npm run add-target ts\n' +
|
|
357
|
+
'then regenerate.')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const Name = model.const.Name // Solardemo
|
|
361
|
+
const lower = String(model.const.name) // solardemo
|
|
362
|
+
const ENV = envName(model) // SOLARDEMO
|
|
363
|
+
const sdkClass = `${Name}SDK` // SolardemoSDK
|
|
364
|
+
const pluginName = `${Name}Provider` // SolardemoProvider
|
|
365
|
+
const fileBase = `${lower}-provider` // solardemo-provider
|
|
366
|
+
|
|
367
|
+
// The SDK is a PUBLISHED dependency, not a path: this package lives in its
|
|
368
|
+
// own repo. Its name is whatever the ts target publishes under, pin
|
|
369
|
+
// included, so the two can never disagree.
|
|
370
|
+
// The TypeScript SDK this provider WRAPS — a different target, so it
|
|
371
|
+
// keeps its own name and does not follow this provider's alias.
|
|
372
|
+
const sdkPkg = packageName(model, 'npm')
|
|
373
|
+
const sdkVersion = packageVersion(model, 'ts')
|
|
374
|
+
|
|
375
|
+
// UNFILTERED by design — see the AGENTS.md sharp edge: entityCollection is
|
|
376
|
+
// the resolver every component must use (getModelPath rebuilds its container
|
|
377
|
+
// per call, defeating the class-name memo), and it deliberately includes
|
|
378
|
+
// inactive entities because the typed-model emitters need them.
|
|
379
|
+
//
|
|
380
|
+
// Which makes filtering `active` the CALLER's job, and this component was
|
|
381
|
+
// the one consumer target that skipped it — go-cli, go-mcp and py-data all
|
|
382
|
+
// re-filter. The cost: MainEntity_ts emits an accessor only for an ACTIVE
|
|
383
|
+
// entity, so an inactive one produced `this.shared.sdk.Ghost()` in the
|
|
384
|
+
// provider and an `assert.equal(typeof sdk.Ghost, 'function')` in its tests,
|
|
385
|
+
// against a method the SDK does not have.
|
|
386
|
+
const entityColl = entityCollection(model)
|
|
387
|
+
|
|
388
|
+
const activeEntities = Object.keys(entityColl).sort()
|
|
389
|
+
.map((key: string) => entityColl[key])
|
|
390
|
+
.filter((ent: any) => false !== ent.active)
|
|
391
|
+
|
|
392
|
+
// Entities this provider can serve: those with at least one op that maps to
|
|
393
|
+
// a Seneca store cmd. An entity with no such op would produce an empty cmd
|
|
394
|
+
// map, which seneca-entity treats as a store that answers nothing.
|
|
395
|
+
const entityNames = activeEntities.map((ent: any) => ent.name)
|
|
396
|
+
|
|
397
|
+
const entities = activeEntities
|
|
398
|
+
.map((ent: any) => {
|
|
399
|
+
const parents = parentKeys(ent)
|
|
400
|
+
|
|
401
|
+
// The parent entity PER KEY. Deriving it from `parents[0]` alone left an
|
|
402
|
+
// entity nested two levels deep with no cross-reference for its outer
|
|
403
|
+
// parents, so their seed values came out as the literal '0'.
|
|
404
|
+
const parentOf: Record<string, string> = {}
|
|
405
|
+
for (const key of parents) {
|
|
406
|
+
parentOf[key] = parentEntityOf(key, entityNames)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// The parent keys of each op SEPARATELY. The guards used to be the union
|
|
410
|
+
// across all ops, applied to every cmd alike, while the argument handed
|
|
411
|
+
// to the SDK was computed per op — so an entity whose routes are not
|
|
412
|
+
// uniformly nested (a flat `load`, a nested `create`) demanded a
|
|
413
|
+
// parameter its own call would never use.
|
|
414
|
+
const opParents: Record<string, string[]> = {}
|
|
415
|
+
for (const opname of entityOps(ent)) {
|
|
416
|
+
opParents[opname] = opParentKeys(ent, opname)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
ent,
|
|
421
|
+
name: ent.name,
|
|
422
|
+
// The SDK ACCESSOR on the client (`client.Moon()`), which is the
|
|
423
|
+
// entity's PascalCase name — NOT entityClassName, which is the
|
|
424
|
+
// collision-safe CLASS name the accessor constructs (`MoonEntity`).
|
|
425
|
+
// Calling the class name reads plausibly and fails at runtime with
|
|
426
|
+
// "sdk.MoonEntity is not a function". MainEntity_ts is the authority
|
|
427
|
+
// for this: it declares the method as `${entity.Name}()`.
|
|
428
|
+
acc: ent.Name,
|
|
429
|
+
// The entity's canonical route, used to probe the live server for
|
|
430
|
+
// liveness. Path params are left in place only if the route has
|
|
431
|
+
// them — a collection route (the `list` op's) has none, which is why
|
|
432
|
+
// entityPath prefers it.
|
|
433
|
+
path: entityPath(ent),
|
|
434
|
+
cls: entityClassName(ent, entityColl),
|
|
435
|
+
ops: entityOps(ent),
|
|
436
|
+
idf: entityIdField(ent),
|
|
437
|
+
parents,
|
|
438
|
+
parentOf,
|
|
439
|
+
opParents,
|
|
440
|
+
// The entity the FIRST parent key points at. Kept because the docs and
|
|
441
|
+
// the scripts speak about "the parent" in the singular; anything that
|
|
442
|
+
// must be right per key reads parentOf.
|
|
443
|
+
parentEntity: 0 < parents.length ? parentOf[parents[0]] : '',
|
|
444
|
+
// The custom actions this entity exposes, as cmd -> action -> SDK
|
|
445
|
+
// op. Emitted as a map in the plugin so a handler can route
|
|
446
|
+
// `action$` to the op that actually serves it, and so an unknown
|
|
447
|
+
// name can be refused with the valid ones named.
|
|
448
|
+
actions: Object.keys(CMD_OPS).sort().reduce(
|
|
449
|
+
(acc: Record<string, Record<string, string>>, cmd: string) => {
|
|
450
|
+
acc[cmd] = cmdActions(ent, cmd)
|
|
451
|
+
return acc
|
|
452
|
+
}, {}),
|
|
453
|
+
// The same information flattened, for the README and the generated
|
|
454
|
+
// tests: [{ cmd, op, action, path }].
|
|
455
|
+
actionList: entityActionList(ent),
|
|
456
|
+
// The ops with a route of their own. `ops` minus those that are
|
|
457
|
+
// nothing but folded-in actions — what the generated tests consult
|
|
458
|
+
// before assuming a plain call exists.
|
|
459
|
+
canonicalOps: canonicalOps(ent),
|
|
460
|
+
// Required fields only: a seed record has to satisfy the shape the
|
|
461
|
+
// SDK will hand back, and optional noise makes the assertions
|
|
462
|
+
// harder to read.
|
|
463
|
+
//
|
|
464
|
+
// A parent path param is then FORCED IN even when the entity's own
|
|
465
|
+
// schema omits it or marks it optional. A path param is a routing key,
|
|
466
|
+
// not necessarily a response field: when the child's schema left it
|
|
467
|
+
// out the seeded record had no link back to its parent, and the mock's
|
|
468
|
+
// match found nothing — which the nested `load` test reported as a
|
|
469
|
+
// TypeError and the nested `list` test reported as a pass.
|
|
470
|
+
fields: (() => {
|
|
471
|
+
const req = (ent.fields || [])
|
|
472
|
+
.filter((f: any) => false !== f.req)
|
|
473
|
+
.map((f: any) => ({
|
|
474
|
+
name: f.name,
|
|
475
|
+
kind: fieldKind(f.type),
|
|
476
|
+
parentEntity: parentEntityOf(f.name, entityNames),
|
|
477
|
+
}))
|
|
478
|
+
|
|
479
|
+
const have = new Set(req.map((f: any) => f.name))
|
|
480
|
+
|
|
481
|
+
for (const key of parents) {
|
|
482
|
+
if (!have.has(key)) {
|
|
483
|
+
req.push({
|
|
484
|
+
name: key,
|
|
485
|
+
kind: 'string',
|
|
486
|
+
parentEntity: parentOf[key],
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return req
|
|
492
|
+
})(),
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
.map((e: any) => ({
|
|
496
|
+
...e,
|
|
497
|
+
cmds: Object.keys(CMD_OPS)
|
|
498
|
+
.filter((cmd) => CMD_OPS[cmd].some((op) => e.ops.includes(op))),
|
|
499
|
+
}))
|
|
500
|
+
.filter((e: any) => 0 < e.cmds.length)
|
|
501
|
+
|
|
502
|
+
if (0 === entities.length) {
|
|
503
|
+
throw new SdkGenError(
|
|
504
|
+
'seneca-provider: no entity in this model declares a list/load/create/' +
|
|
505
|
+
'update/remove operation, so the plugin would expose no entities at ' +
|
|
506
|
+
'all. Remove the target, or add an entity with CRUD ops.')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const repo = providerRepo(model, lower, target.name)
|
|
510
|
+
|
|
511
|
+
// The companion test server lives in the SDK repo's `app/` and is NOT
|
|
512
|
+
// published, so the only way to reach it is the local checkout. The path
|
|
513
|
+
// back to it is the inverse of this target's own `output: path`, computed
|
|
514
|
+
// once by the external pass — see cmp/ExternalTarget.
|
|
515
|
+
const sdkrel = ctx$.sdkrelpath || '..'
|
|
516
|
+
|
|
517
|
+
// Where a live run points — and whether there is anything honest to point
|
|
518
|
+
// it at.
|
|
519
|
+
//
|
|
520
|
+
// NOT simply `servers[0].url`. For an OpenAPI-derived model that is the
|
|
521
|
+
// PRODUCTION host of a third-party API, and everything gated on it aims
|
|
522
|
+
// there: the `describe('live')` block, which `npm test` runs on every CI
|
|
523
|
+
// push on three operating systems; the `serverUp` probe in front of it; and
|
|
524
|
+
// test/quick.js, whose header says "start the companion server first" while
|
|
525
|
+
// BASE silently defaults to production and whose body is a create / update /
|
|
526
|
+
// remove cycle. A live suite that reaches a stranger's API from CI is not a
|
|
527
|
+
// live suite, it is traffic — and unauthenticated traffic at that.
|
|
528
|
+
//
|
|
529
|
+
// So a live base is taken only when it is unambiguously OURS: declared
|
|
530
|
+
// outright by the project, or a loopback address, which no third party can
|
|
531
|
+
// be behind. Anything else leaves live testing ungenerated, which is the
|
|
532
|
+
// honest answer for an API nobody here runs.
|
|
533
|
+
const live = model?.main?.[KIT]?.test?.live || {}
|
|
534
|
+
const servers = (model?.main?.[KIT]?.info?.servers || [])
|
|
535
|
+
const specBase = 0 < servers.length ? String(servers[0].url || '') : ''
|
|
536
|
+
|
|
537
|
+
const loopback = (url: string) =>
|
|
538
|
+
/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])([:/]|$)/i.test(url)
|
|
539
|
+
|
|
540
|
+
const liveBase = null != live.base && '' !== live.base ? String(live.base) :
|
|
541
|
+
(loopback(specBase) ? specBase : '')
|
|
542
|
+
|
|
543
|
+
// Whether the SDK's own repo ships a runnable companion server under
|
|
544
|
+
// `app/`. NOTHING in an API definition says so — it is a property of the
|
|
545
|
+
// sibling project — so it cannot be inferred, and inferring it is what put
|
|
546
|
+
// an unconditional `git clone && cd app && npm install && npm run build`
|
|
547
|
+
// into every generated provider's CI.
|
|
548
|
+
const liveApp = true === live.app || (loopback(specBase) && '' === (live.base || ''))
|
|
549
|
+
|
|
550
|
+
const provider = {
|
|
551
|
+
Name, lower, ENV, sdkClass, pluginName, fileBase,
|
|
552
|
+
sdkPkg, sdkVersion, entities,
|
|
553
|
+
repoUrl: repo.url,
|
|
554
|
+
// The SDK's own repo, for pointing at the companion test server which is
|
|
555
|
+
// only distributed in source.
|
|
556
|
+
sdkRepoUrl: repoInfo(model).repoUrl,
|
|
557
|
+
api: apiName(model),
|
|
558
|
+
version: packageVersion(model, target.name),
|
|
559
|
+
sdkrel,
|
|
560
|
+
liveBase,
|
|
561
|
+
liveApp,
|
|
562
|
+
// The sponsor line. @seneca/maintain's `content_readme` check requires
|
|
563
|
+
// the publisher's name in the README, so this is load-bearing rather
|
|
564
|
+
// than decorative — the generated `maintain` test fails without it.
|
|
565
|
+
publisher: PUBLISHER,
|
|
566
|
+
publisherUrl: PUBLISHER_URL,
|
|
567
|
+
// A route with NO path params, so the probe is a plain GET. An entity
|
|
568
|
+
// whose every route is parameterised gives none, and then the probe
|
|
569
|
+
// falls back to the base URL itself.
|
|
570
|
+
probePath: (entities.find((e: any) =>
|
|
571
|
+
'' !== e.path && !e.path.includes('{')) || { path: '' }).path,
|
|
572
|
+
// The provider's OWN published name. Not derived from the SDK slug the
|
|
573
|
+
// way a language target's is — a provider is `@seneca/<name>-provider` —
|
|
574
|
+
// so read the project's pin directly and default to that shape.
|
|
575
|
+
pkgName: providerPackage(model, lower, target.name),
|
|
576
|
+
// Whether this API authenticates at all. Decides if the plugin plumbs a
|
|
577
|
+
// credential: the SDK's auth stage emits nothing for an auth-inactive
|
|
578
|
+
// model and strips the authorization header regardless of options, so
|
|
579
|
+
// plumbing one anyway produces a credential path that cannot work.
|
|
580
|
+
authActive: isAuthActive(model),
|
|
581
|
+
// Whether this API's scheme is genuine HTTP Basic Auth (two credentials,
|
|
582
|
+
// base64-joined), matching the SDK's own auth.basic signal. Decides
|
|
583
|
+
// whether the plugin also plumbs a `secret` alongside `apikey` — a
|
|
584
|
+
// Basic-Auth SDK's own auth stage deletes the header when either is
|
|
585
|
+
// missing, so a provider forwarding apikey alone could never
|
|
586
|
+
// authenticate, the same class of gap `apikey`-only forwarding was.
|
|
587
|
+
authBasic: isAuthActive(model) && isHttpBasicAuth(model),
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// `.gitignore` is EMITTED rather than copied — npm strips that filename
|
|
591
|
+
// from the tarball, so as a template it reached only checkout users. See
|
|
592
|
+
// Gitignore_seneca-provider. Called before the Copy, as every language
|
|
593
|
+
// target calls its own.
|
|
594
|
+
Gitignore({})
|
|
595
|
+
|
|
596
|
+
// Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, tsfmt.json and
|
|
597
|
+
// both tsconfigs. Same for every provider.
|
|
598
|
+
Copy({
|
|
599
|
+
from: 'tm/' + target.name,
|
|
600
|
+
replace: { ...ctx$.stdrep },
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
PackageJson({ provider, target })
|
|
604
|
+
ProviderSource({ provider })
|
|
605
|
+
ProviderDoc({ provider })
|
|
606
|
+
Tests({ provider })
|
|
607
|
+
Scripts({ provider })
|
|
608
|
+
Workflow({ provider })
|
|
609
|
+
Readme({ provider })
|
|
610
|
+
Docs({ provider })
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
// --- package.json -----------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
const PackageJson = cmp(function PackageJson(props: any) {
|
|
617
|
+
const { provider, target } = props
|
|
618
|
+
const { model } = props.ctx$
|
|
619
|
+
|
|
620
|
+
const deps = collectDeps(model, target.name, target.deps, props.ctx$.log)
|
|
621
|
+
|
|
622
|
+
// collectDeps returns { name, version, source, raw } — the DECLARED kind
|
|
623
|
+
// (prod/peer/dev) is on `raw`, not hoisted. Reading `d.kind` instead
|
|
624
|
+
// silently yields an empty manifest section, which is how the first cut of
|
|
625
|
+
// this component shipped a package.json with no seneca peers at all.
|
|
626
|
+
//
|
|
627
|
+
// `kind` is a COMMA-SEPARATED LIST, so one dependency can land in more than
|
|
628
|
+
// one manifest section. That is the Seneca plugin convention and not an
|
|
629
|
+
// embellishment: `seneca` is a PEER (the plugin must run inside the host's
|
|
630
|
+
// instance, never its own bundled copy) and also a DEV dependency (the test
|
|
631
|
+
// suite does `require('seneca')` directly, so a bare `npm install` in a
|
|
632
|
+
// clean checkout has to produce it). collectDeps deduplicates by package
|
|
633
|
+
// name and the model's dep map is keyed by name, so the same package cannot
|
|
634
|
+
// be declared twice — the kind has to carry the list instead.
|
|
635
|
+
const dep = (kind: string) => {
|
|
636
|
+
const out: Record<string, string> = {}
|
|
637
|
+
const kinds = (d: any) => String((d.raw && d.raw.kind) || '')
|
|
638
|
+
.split(',').map((s: string) => s.trim()).filter((s: string) => '' !== s)
|
|
639
|
+
|
|
640
|
+
for (const d of deps.filter((d: any) => kinds(d).includes(kind))) {
|
|
641
|
+
out[d.name] = d.version
|
|
642
|
+
}
|
|
643
|
+
return out
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Attribution. Model-driven, because it cannot be derived and because
|
|
647
|
+
// regeneration OVERWRITES the manifest: the hand-written provider this
|
|
648
|
+
// target was modelled on lost its author and both named contributors on the
|
|
649
|
+
// first regeneration, and nothing failed. Unset falls back to the publisher.
|
|
650
|
+
const author = authorInfo(model)
|
|
651
|
+
const contributors = contributorList(model)
|
|
652
|
+
|
|
653
|
+
const pkg = {
|
|
654
|
+
name: provider.pkgName,
|
|
655
|
+
version: provider.version,
|
|
656
|
+
main: `dist/${provider.fileBase}.js`,
|
|
657
|
+
type: 'commonjs',
|
|
658
|
+
types: `dist/${provider.fileBase}.d.ts`,
|
|
659
|
+
description:
|
|
660
|
+
`Seneca entity provider for the ${provider.api} API, using the ` +
|
|
661
|
+
`${provider.sdkPkg} SDK.`,
|
|
662
|
+
homepage: provider.repoUrl,
|
|
663
|
+
keywords: ['seneca', provider.lower, `${provider.lower}-provider`,
|
|
664
|
+
provider.publisher.toLowerCase(), 'sdk'],
|
|
665
|
+
author,
|
|
666
|
+
// Omitted entirely when the project names none, rather than emitted as an
|
|
667
|
+
// empty array — npm treats `"contributors": []` as a declaration that
|
|
668
|
+
// there are none, which is a different claim from not having said.
|
|
669
|
+
...(0 < contributors.length ? { contributors } : {}),
|
|
670
|
+
license: 'MIT',
|
|
671
|
+
repository: { type: 'git', url: `git+${provider.repoUrl}.git` },
|
|
672
|
+
scripts: {
|
|
673
|
+
test: 'node --enable-source-maps --test test/**/*.test.js',
|
|
674
|
+
'test-some':
|
|
675
|
+
'node --enable-source-maps --test-name-pattern="$TEST_PATTERN" ' +
|
|
676
|
+
'--test "test/**/*.test.js"',
|
|
677
|
+
'test-watch': 'node --test --watch test/**/*.test.js',
|
|
678
|
+
watch: 'tsc --build src test -w',
|
|
679
|
+
build: 'tsc --build src test',
|
|
680
|
+
'test-coverage':
|
|
681
|
+
'node --enable-source-maps --experimental-test-coverage --test ' +
|
|
682
|
+
'test/**/*.test.js',
|
|
683
|
+
clean:
|
|
684
|
+
'rm -rf node_modules dist dist-test .tsbuildinfo yarn.lock ' +
|
|
685
|
+
'package-lock.json',
|
|
686
|
+
reset: 'npm run clean && npm i && npm run build && npm test',
|
|
687
|
+
// The Seneca release convention: tag from package.json, push, publish.
|
|
688
|
+
// Generated because a provider is released like every other Seneca
|
|
689
|
+
// plugin, and a maintainer who has to remember the incantation will
|
|
690
|
+
// eventually publish an untested build.
|
|
691
|
+
'repo-tag':
|
|
692
|
+
'REPO_VERSION=`node -e "console.log(require(\'./package\').version)"` ' +
|
|
693
|
+
'&& echo TAG: v$REPO_VERSION && git commit -a -m v$REPO_VERSION ' +
|
|
694
|
+
'&& git push && git tag v$REPO_VERSION && git push --tags;',
|
|
695
|
+
'repo-publish': 'npm run clean && npm i && npm run repo-publish-quick',
|
|
696
|
+
'repo-publish-quick':
|
|
697
|
+
'npm run build && npm run test && npm run repo-tag && ' +
|
|
698
|
+
'npm publish --access public --registry https://registry.npmjs.org',
|
|
699
|
+
},
|
|
700
|
+
// What actually ships. Without `files`, `npm publish` packs the test
|
|
701
|
+
// suite and build output into the tarball.
|
|
702
|
+
files: ['dist', 'src/**/*.ts', 'LICENSE'],
|
|
703
|
+
engines: { node: '>=24' },
|
|
704
|
+
dependencies: {
|
|
705
|
+
// The SDK this plugin wraps, by its PUBLISHED name and version.
|
|
706
|
+
[provider.sdkPkg]: `^${provider.sdkVersion}`,
|
|
707
|
+
...dep('prod'),
|
|
708
|
+
},
|
|
709
|
+
peerDependencies: dep('peer'),
|
|
710
|
+
devDependencies: dep('dev'),
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
File({ name: 'package.json' }, () => {
|
|
714
|
+
Content(JSON.stringify(pkg, null, 2) + '\n')
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
// --- src/<name>-provider.ts -------------------------------------------------
|
|
720
|
+
|
|
721
|
+
const ProviderSource = cmp(function ProviderSource(props: any) {
|
|
722
|
+
const { provider } = props
|
|
723
|
+
|
|
724
|
+
Folder({ name: 'src' }, () => {
|
|
725
|
+
File({ name: `${provider.fileBase}.ts` }, () => {
|
|
726
|
+
Content(`/* Generated by @voxgig/sdkgen. Do not edit. */
|
|
727
|
+
|
|
728
|
+
const Pkg = require('../package.json')
|
|
729
|
+
|
|
730
|
+
const { ${provider.sdkClass} } = require('${provider.sdkPkg}')
|
|
731
|
+
|
|
732
|
+
const SdkPkg = require('${provider.sdkPkg}/package.json')
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
type ${provider.pluginName}Options = {
|
|
736
|
+
// Options passed straight to the ${provider.sdkClass} constructor,
|
|
737
|
+
// most usefully \`base\` to point at a server.
|
|
738
|
+
sdk?: Record<string, any>
|
|
739
|
+
|
|
740
|
+
// Run the SDK in offline test mode (in-memory mock transport).
|
|
741
|
+
test?: boolean
|
|
742
|
+
|
|
743
|
+
// Test feature options, e.g. {entity: {${provider.entities[0].name}: {...}}} to
|
|
744
|
+
// seed the mock with data. Only used when \`test\` is true.
|
|
745
|
+
testopts?: Record<string, any>
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
function ${provider.pluginName}(this: any, options: ${provider.pluginName}Options) {
|
|
750
|
+
const seneca: any = this
|
|
751
|
+
|
|
752
|
+
const entityBuilder = this.export('provider/entityBuilder')
|
|
753
|
+
|
|
754
|
+
seneca.message('sys:provider,provider:${provider.lower},get:info', get_info)
|
|
755
|
+
|
|
756
|
+
async function get_info(this: any, _msg: any) {
|
|
757
|
+
return {
|
|
758
|
+
ok: true,
|
|
759
|
+
name: '${provider.lower}',
|
|
760
|
+
version: Pkg.version,
|
|
761
|
+
sdk: {
|
|
762
|
+
name: '${provider.sdkPkg}',
|
|
763
|
+
version: SdkPkg.version,
|
|
764
|
+
},
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
// Every SDK operation resolves to an SDK ENTITY rather than raw data (a
|
|
770
|
+
// removed record included: it comes back marked deleted, still holding what
|
|
771
|
+
// it held). Seneca wants plain data, which the entity hands over through
|
|
772
|
+
// data().
|
|
773
|
+
function plain(res: any) {
|
|
774
|
+
return null == res ? res : res.data()
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
// Seneca query directives (sort$, limit$, ...) are for the store, not the
|
|
779
|
+
// API, so they must not reach the SDK as match fields.
|
|
780
|
+
function cleanq(q: any) {
|
|
781
|
+
const out: any = {}
|
|
782
|
+
for (const k in (q || {})) {
|
|
783
|
+
if (!k.endsWith('$')) {
|
|
784
|
+
out[k] = q[k]
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return out
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
// \`action$\` — the directive that selects a custom API action instead of
|
|
792
|
+
// the plain cmd. READ BEFORE cleanq strips it, and still stripped from
|
|
793
|
+
// what reaches the SDK as match fields: it is an instruction to the store,
|
|
794
|
+
// like sort$ and limit$, not a value to filter on.
|
|
795
|
+
//
|
|
796
|
+
// THREE PLACES, because seneca-entity puts it in three places depending on
|
|
797
|
+
// how the caller spelled it, and dropping any one of them silently turns a
|
|
798
|
+
// named action into an ordinary call:
|
|
799
|
+
//
|
|
800
|
+
// list$/load$/remove$({ action$ }) -> msg.q.action$
|
|
801
|
+
// ent.directive$({ action$ }).save$() -> msg.action$
|
|
802
|
+
// const e = ent.make$({...}); e.action$=.. -> msg.ent.action$
|
|
803
|
+
//
|
|
804
|
+
// \`make$({ action$ })\` is NOT among them and cannot be: seneca-entity's
|
|
805
|
+
// make$ copies only keys without a \`$\`, plus the four directives it knows
|
|
806
|
+
// (id$, merge$, custom$, directive$), so an unknown trailing-\`$\` key is
|
|
807
|
+
// dropped before any store sees it. \`id$\` reads like the precedent for
|
|
808
|
+
// one, but it works only because make$ names it explicitly. The README
|
|
809
|
+
// says so; there is nothing this plugin can check, because nothing arrives.
|
|
810
|
+
function actionOf(msg: any) {
|
|
811
|
+
const q = msg && msg.q
|
|
812
|
+
const ent = msg && msg.ent
|
|
813
|
+
|
|
814
|
+
return null != (q && q.action$) ? q.action$ :
|
|
815
|
+
null != (msg && msg.action$) ? msg.action$ :
|
|
816
|
+
null != (ent && ent.action$) ? ent.action$ :
|
|
817
|
+
undefined
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
// The SDK argument for an ACTION on a read cmd: everything the caller sent
|
|
822
|
+
// minus Seneca's own directives, with the record key carried across, plus
|
|
823
|
+
// the \`$action\` selector the SDK dispatches on.
|
|
824
|
+
//
|
|
825
|
+
// WIDER than the canonical argument on purpose. An action route has its own
|
|
826
|
+
// parameters — GitHub's merge takes commit_title and merge_method, which the
|
|
827
|
+
// canonical PATCH knows nothing about — so narrowing to the plain op's
|
|
828
|
+
// required keys would strip the action's whole payload. The SDK still
|
|
829
|
+
// validates: an action whose own point cannot be built is refused with
|
|
830
|
+
// \`point_action_invalid\` rather than sent somewhere else.
|
|
831
|
+
function actionq(q: any, rk: string, action: string) {
|
|
832
|
+
const out = cleanq(q)
|
|
833
|
+
|
|
834
|
+
if ('id' !== rk && null != out.id) {
|
|
835
|
+
out[rk] = out.id
|
|
836
|
+
delete out.id
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
out.$action = action
|
|
840
|
+
return out
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
// The SDK throws on any non-2xx. A 404 from a single-item read is an
|
|
845
|
+
// ordinary "not found" answer rather than a failure, so return null and let
|
|
846
|
+
// everything else propagate. SDK errors carry the HTTP status at the top
|
|
847
|
+
// level, so ask them rather than digging into \`result\`.
|
|
848
|
+
async function ornull(action: () => Promise<any>) {
|
|
849
|
+
try {
|
|
850
|
+
return await action()
|
|
851
|
+
}
|
|
852
|
+
catch (e: any) {
|
|
853
|
+
if (true === e?.notFound) {
|
|
854
|
+
return null
|
|
855
|
+
}
|
|
856
|
+
throw e
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
`)
|
|
861
|
+
|
|
862
|
+
// A guard per required parent key. Without it the SDK builds a
|
|
863
|
+
// half-formed URL and the caller gets an opaque 404 instead of being
|
|
864
|
+
// told what they left out.
|
|
865
|
+
const guarded = provider.entities.filter((e: any) => 0 < e.parents.length)
|
|
866
|
+
if (0 < guarded.length) {
|
|
867
|
+
Content(` // Nested entities cannot build their path without the parent id, so
|
|
868
|
+
// say which key is missing rather than letting the SDK report an opaque
|
|
869
|
+
// 404 on a half-built URL.
|
|
870
|
+
`)
|
|
871
|
+
each(guarded, (e: any) => {
|
|
872
|
+
each(e.parents, (key: any) => {
|
|
873
|
+
const k = String(key.val$ ?? key)
|
|
874
|
+
Content(` function ${guardName(e, k)}(value: any, cmd: string) {
|
|
875
|
+
if (null == value || '' === value) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
'${provider.pkgName}: ${e.name} ' + cmd + ': ${k} is required'
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
return value
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
`)
|
|
885
|
+
})
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Seneca's own entity key is ALWAYS literally `id` — `load$('x')` sets
|
|
890
|
+
// `q = { id: 'x' }`, and the id of the entity it builds comes off
|
|
891
|
+
// `data.id`. An API that addresses a record by anything else therefore
|
|
892
|
+
// needs translating in both directions, or `load$` requests a record
|
|
893
|
+
// keyed `undefined` and every entity handed back has no id at all — one
|
|
894
|
+
// that cannot then be saved or removed.
|
|
895
|
+
const aliased = provider.entities.filter((e: any) => 'id' !== recordKey(e.ent))
|
|
896
|
+
each(aliased, (e: any) => {
|
|
897
|
+
const rk = recordKey(e.ent)
|
|
898
|
+
Content(` // This API keys a ${e.name} by \`${rk}\`, Seneca by \`id\`. Carry the
|
|
899
|
+
// API's key across so the Seneca entity has one.
|
|
900
|
+
function id_${e.name}(data: any) {
|
|
901
|
+
if (null != data && null == data.id) {
|
|
902
|
+
data.id = ${jsProp('data', rk)}
|
|
903
|
+
}
|
|
904
|
+
return data
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
`)
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
// WHICH SDK OP SERVES EACH `action$`, per entity and per cmd.
|
|
912
|
+
//
|
|
913
|
+
// Emitted for EVERY cmd, including the ones with no actions at all.
|
|
914
|
+
// That empty map is not waste: it is what lets `actionop` refuse an
|
|
915
|
+
// `action$` on an entity that has none, instead of ignoring the key
|
|
916
|
+
// and performing an ordinary call. Passing `action$` and getting a
|
|
917
|
+
// plain save is the failure this whole mechanism exists to prevent —
|
|
918
|
+
// it is how GitHub's `merge` silently became an "update".
|
|
919
|
+
Content(` // The custom actions each cmd can reach, as action -> SDK op. An action
|
|
920
|
+
// is an alternative POINT of an ordinary op (\`select.$action\` in the API
|
|
921
|
+
// model), so \`save$\` routes by this map rather than assuming update: an
|
|
922
|
+
// action folded into \`create\` is reached through \`save$\` too.
|
|
923
|
+
const ACTIONS: Record<string, Record<string, Record<string, string>>> = {
|
|
924
|
+
`)
|
|
925
|
+
each(provider.entities, (e: any) => {
|
|
926
|
+
Content(` [${JSON.stringify(e.name)}]: {
|
|
927
|
+
`)
|
|
928
|
+
each(e.cmds, (cmd: any) => {
|
|
929
|
+
const name = String(cmd.val$ ?? cmd)
|
|
930
|
+
const map = e.actions[name] || {}
|
|
931
|
+
const names = Object.keys(map).sort()
|
|
932
|
+
// COMPUTED KEYS, not `jsKey`. An action named `__proto__` written
|
|
933
|
+
// as a plain (or quoted) object-literal key SETS THE PROTOTYPE
|
|
934
|
+
// instead of creating a property, so the action would vanish from
|
|
935
|
+
// its own map and be unreachable. A computed key always defines an
|
|
936
|
+
// own property. apidef derives an action name from a route segment,
|
|
937
|
+
// and `__proto__` is a legal one.
|
|
938
|
+
Content(` ${name}: {${names.map((a: string) =>
|
|
939
|
+
` [${JSON.stringify(a)}]: '${map[a]}'`).join(',')}${0 < names.length ? ' ' : ''}},
|
|
940
|
+
`)
|
|
941
|
+
})
|
|
942
|
+
Content(` },
|
|
943
|
+
`)
|
|
944
|
+
})
|
|
945
|
+
Content(` }
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
// Resolve an \`action$\` to the SDK op that serves it, or REFUSE it.
|
|
949
|
+
//
|
|
950
|
+
// Never falls through to the ordinary call. An action name the entity does
|
|
951
|
+
// not have is a caller mistake worth a message that names what is
|
|
952
|
+
// available; performing a plain save instead is the one outcome that must
|
|
953
|
+
// not happen, because it succeeds and does the wrong thing.
|
|
954
|
+
// AN OWN PROPERTY, never an inherited one. \`map[name]\` resolves
|
|
955
|
+
// \`toString\`, \`constructor\`, \`valueOf\` and the rest off Object's
|
|
956
|
+
// prototype, and each of those is non-null — so the refusal below never
|
|
957
|
+
// fired and the inherited function was handed to the SDK as an op name.
|
|
958
|
+
// That is the silent drop in another hat: the caller named something the
|
|
959
|
+
// entity does not have and was not told. An empty map inherits them all,
|
|
960
|
+
// so a cmd with no actions was the most exposed.
|
|
961
|
+
function actionop(name: string, entname: string, cmd: string) {
|
|
962
|
+
const own = Object.prototype.hasOwnProperty
|
|
963
|
+
const ents: any = own.call(ACTIONS, entname) ? ACTIONS[entname] : {}
|
|
964
|
+
const map: any = own.call(ents, cmd) ? ents[cmd] : {}
|
|
965
|
+
const op = own.call(map, name) ? map[name] : null
|
|
966
|
+
|
|
967
|
+
if (null == op) {
|
|
968
|
+
const valid = Object.keys(map).sort()
|
|
969
|
+
throw new Error(
|
|
970
|
+
'${provider.pkgName}: ' + entname + ' ' + cmd + ': action$ "' + name +
|
|
971
|
+
'" is not an action of this operation. Valid: ' +
|
|
972
|
+
(0 < valid.length ? valid.join(', ') : '(none)'))
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return op
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
`)
|
|
980
|
+
|
|
981
|
+
// The cmd map, declared up front so every action is attached to a
|
|
982
|
+
// shape seneca-entity can read before the actions are defined.
|
|
983
|
+
Content(` const entity: any = {
|
|
984
|
+
`)
|
|
985
|
+
each(provider.entities, (e: any) => {
|
|
986
|
+
Content(` ${jsKey(e.name)}: {
|
|
987
|
+
cmd: {
|
|
988
|
+
`)
|
|
989
|
+
each(e.cmds, (cmd: any) => {
|
|
990
|
+
Content(` ${String(cmd.val$ ?? cmd)}: { action: (undefined as any) },
|
|
991
|
+
`)
|
|
992
|
+
})
|
|
993
|
+
Content(` },
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
`)
|
|
997
|
+
})
|
|
998
|
+
Content(` }
|
|
999
|
+
|
|
1000
|
+
`)
|
|
1001
|
+
|
|
1002
|
+
each(provider.entities, (e: any) => {
|
|
1003
|
+
// The guard set is PER OP, not the union across the entity's ops. An
|
|
1004
|
+
// entity whose routes are not uniformly nested — a flat `load`, a
|
|
1005
|
+
// nested `create` — used to demand the parent id on the flat call too,
|
|
1006
|
+
// an argument its own SDK request would then never use.
|
|
1007
|
+
//
|
|
1008
|
+
// `save` guards the union of create's and update's keys: one action
|
|
1009
|
+
// serves both and dispatches at runtime, so it cannot know which set
|
|
1010
|
+
// applies until it has the data.
|
|
1011
|
+
const guard = (cmd: string, src: string) => {
|
|
1012
|
+
const keys = 'save' === cmd ?
|
|
1013
|
+
[...new Set([...(e.opParents.create || []), ...(e.opParents.update || [])])].sort() :
|
|
1014
|
+
(e.opParents[cmd] || [])
|
|
1015
|
+
|
|
1016
|
+
return keys
|
|
1017
|
+
.map((k: string) =>
|
|
1018
|
+
` ${guardName(e, k)}(${jsProp(src, k)}, '${cmd}')\n`)
|
|
1019
|
+
.join('')
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Reading the record's own key off the Seneca query, which always
|
|
1023
|
+
// spells it `id`, and every other required key off its own name.
|
|
1024
|
+
const rk = recordKey(e.ent)
|
|
1025
|
+
const sdkArg = (opname: string) => {
|
|
1026
|
+
const keys = requiredKeys(e.ent, opname)
|
|
1027
|
+
if (0 === keys.length) {
|
|
1028
|
+
return '{}'
|
|
1029
|
+
}
|
|
1030
|
+
return `{ ${keys.map((k: string) =>
|
|
1031
|
+
`${jsKey(k)}: ${jsProp('q', k === rk ? 'id' : k)}`).join(', ')} }`
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// The data hop, plus the id alias when the API keys the record by
|
|
1035
|
+
// something other than `id`.
|
|
1036
|
+
const out = (expr: string) =>
|
|
1037
|
+
'id' === rk ? `plain(${expr})` : `id_${e.name}(plain(${expr}))`
|
|
1038
|
+
|
|
1039
|
+
// The action branch, emitted for every cmd whether or not this entity
|
|
1040
|
+
// has actions. `actionop` is what refuses an unknown name, so leaving
|
|
1041
|
+
// it out where the map is empty would restore the silent drop for
|
|
1042
|
+
// exactly the entities most likely to be typed at by mistake.
|
|
1043
|
+
//
|
|
1044
|
+
// IT COMES BEFORE THE PARENT GUARDS, and that ordering is the whole
|
|
1045
|
+
// of its correctness. The guards describe the CANONICAL route —
|
|
1046
|
+
// opParams drops action points when computing them — and an action
|
|
1047
|
+
// route need not be nested the same way: Zoom's canonical update is
|
|
1048
|
+
// `/user/{user_id}/meeting/{id}` while its status action hangs off
|
|
1049
|
+
// `/meeting/{id}`. Guarding first rejected that action for want of a
|
|
1050
|
+
// `user_id` its own URL has no segment for.
|
|
1051
|
+
//
|
|
1052
|
+
// The action path is not left unguarded, it is guarded by the RIGHT
|
|
1053
|
+
// thing: the SDK builds the action's own point and refuses an
|
|
1054
|
+
// unbuildable one with `point_action_invalid`, naming the op and the
|
|
1055
|
+
// action. A call naming no action falls through to the guards exactly
|
|
1056
|
+
// as before.
|
|
1057
|
+
const actionBranch = (cmd: string, call: string) => ` const action$ = actionOf(msg)
|
|
1058
|
+
if (null != action$) {
|
|
1059
|
+
const op$ = actionop(action$, '${e.name}', '${cmd}')
|
|
1060
|
+
${call} }
|
|
1061
|
+
|
|
1062
|
+
`
|
|
1063
|
+
|
|
1064
|
+
if (e.cmds.includes('list')) {
|
|
1065
|
+
Content(`
|
|
1066
|
+
${jsProp('entity', e.name)}.cmd.list.action =
|
|
1067
|
+
async function list_${e.name}(this: any, entize: any, msg: any) {
|
|
1068
|
+
const q = cleanq(msg.q)
|
|
1069
|
+
${actionBranch('list',
|
|
1070
|
+
` const found = await this.shared.sdk.${e.acc}()[op$](actionq(msg.q, '${rk}', action$))
|
|
1071
|
+
return found.map((data: any) => entize(${out('data')}))
|
|
1072
|
+
`)}${guard('list', 'q')} const list = await this.shared.sdk.${e.acc}().list(q)
|
|
1073
|
+
return list.map((data: any) => entize(${out('data')}))
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
`)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (e.cmds.includes('load')) {
|
|
1080
|
+
Content(`
|
|
1081
|
+
${jsProp('entity', e.name)}.cmd.load.action =
|
|
1082
|
+
async function load_${e.name}(this: any, entize: any, msg: any) {
|
|
1083
|
+
const q = cleanq(msg.q)
|
|
1084
|
+
${actionBranch('load',
|
|
1085
|
+
` const hit = await ornull(() => this.shared.sdk.${e.acc}()[op$](actionq(msg.q, '${rk}', action$)))
|
|
1086
|
+
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')})
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
`)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (e.cmds.includes('save')) {
|
|
1095
|
+
const hasCreate = e.ops.includes('create')
|
|
1096
|
+
const hasUpdate = e.ops.includes('update')
|
|
1097
|
+
|
|
1098
|
+
// Dispatch on the SENECA key. The record arriving here is a Seneca
|
|
1099
|
+
// entity's data, so its id lives at `id` whatever the API calls it —
|
|
1100
|
+
// dispatching on the API's key sent every save to `create`, leaving
|
|
1101
|
+
// update unreachable.
|
|
1102
|
+
const body = hasCreate && hasUpdate
|
|
1103
|
+
? ` const res = null == data.id
|
|
1104
|
+
? await sdk.${e.acc}().create(data)
|
|
1105
|
+
: await sdk.${e.acc}().update(data)`
|
|
1106
|
+
: hasCreate
|
|
1107
|
+
? ` const res = await sdk.${e.acc}().create(data)`
|
|
1108
|
+
: ` const res = await sdk.${e.acc}().update(data)`
|
|
1109
|
+
|
|
1110
|
+
// ... and hand the API back its own key, which Seneca does not know
|
|
1111
|
+
// to send.
|
|
1112
|
+
const alias = 'id' === rk ? '' :
|
|
1113
|
+
`
|
|
1114
|
+
// This API keys a ${e.name} by \`${rk}\`; Seneca carries it as \`id\`.
|
|
1115
|
+
if (null == ${jsProp('data', rk)} && null != data.id) {
|
|
1116
|
+
${jsProp('data', rk)} = data.id
|
|
1117
|
+
}
|
|
1118
|
+
`
|
|
1119
|
+
|
|
1120
|
+
Content(`
|
|
1121
|
+
${jsProp('entity', e.name)}.cmd.save.action =
|
|
1122
|
+
async function save_${e.name}(this: any, entize: any, msg: any) {
|
|
1123
|
+
const data = msg.ent.data$(false)
|
|
1124
|
+
${alias} const sdk = this.shared.sdk
|
|
1125
|
+
|
|
1126
|
+
${actionBranch('save',
|
|
1127
|
+
` // The action's OWN payload is the entity's own fields — data$(false)
|
|
1128
|
+
// has already dropped every trailing-\`$\` key, \`action$\` included,
|
|
1129
|
+
// so \`$action\` is the only thing added here.
|
|
1130
|
+
data.$action = action$
|
|
1131
|
+
const done = await sdk.${e.acc}()[op$](data)
|
|
1132
|
+
return entize(${out('done')})
|
|
1133
|
+
`)}${guard('save', 'data')}${body}
|
|
1134
|
+
|
|
1135
|
+
return entize(${out('res')})
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
`)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (e.cmds.includes('remove')) {
|
|
1142
|
+
// A REMOVE ACTION ANSWERS FOR ITSELF. The canonical remove has
|
|
1143
|
+
// nothing to hand back — the record is gone — so its handler
|
|
1144
|
+
// returns null and takes no `entize`. An action folded into
|
|
1145
|
+
// `remove` is a different endpoint with a response of its own
|
|
1146
|
+
// (`/meeting/{id}/archive` answers with the archive record), and
|
|
1147
|
+
// the other three cmds all pass an action's response through.
|
|
1148
|
+
// Applying the canonical "return null" to it threw that away, so
|
|
1149
|
+
// the action appeared to succeed and yielded nothing.
|
|
1150
|
+
//
|
|
1151
|
+
// `entize` is therefore named, never `_entize`. Naming it
|
|
1152
|
+
// conditionally on the entity HAVING a remove action does not work
|
|
1153
|
+
// and the type-check says so: the action branch is emitted for
|
|
1154
|
+
// every entity — that is what refuses an unknown `action$` on one
|
|
1155
|
+
// with no actions — so the parameter is always referenced.
|
|
1156
|
+
Content(`
|
|
1157
|
+
${jsProp('entity', e.name)}.cmd.remove.action =
|
|
1158
|
+
async function remove_${e.name}(this: any, entize: any, msg: any) {
|
|
1159
|
+
const q = cleanq(msg.q)
|
|
1160
|
+
${actionBranch('remove',
|
|
1161
|
+
` const gone = await ornull(() => this.shared.sdk.${e.acc}()[op$](actionq(msg.q, '${rk}', action$)))
|
|
1162
|
+
return null == gone ? null : entize(${out('gone')})
|
|
1163
|
+
`)}${guard('remove', 'q')} await ornull(() => this.shared.sdk.${e.acc}().remove(${sdkArg('remove')}))
|
|
1164
|
+
return null
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
`)
|
|
1168
|
+
}
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
Content(`
|
|
1172
|
+
entityBuilder(this, {
|
|
1173
|
+
provider: {
|
|
1174
|
+
name: '${provider.lower}',
|
|
1175
|
+
},
|
|
1176
|
+
entity
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
seneca.prepare(async function(this: any) {
|
|
1181
|
+
const sdkopts: any = Object.assign({}, options.sdk)
|
|
1182
|
+
${provider.authActive ? `
|
|
1183
|
+
// The provider convention carries credentials, so honour an \`apikey\`
|
|
1184
|
+
// when one is configured and stay quiet when it is not.
|
|
1185
|
+
const res = await this.post('sys:provider,get:keymap,provider:${provider.lower}')
|
|
1186
|
+
const apikey = res?.keymap?.apikey?.value
|
|
1187
|
+
|
|
1188
|
+
// Hand the credential to the SDK as \`apikey\`, NOT as an authorization
|
|
1189
|
+
// HEADER. The SDK's own auth stage owns that header: it reads
|
|
1190
|
+
// \`options.apikey\`, and on every path where it finds none it DELETES
|
|
1191
|
+
// \`authorization\` before the request goes out. A provider that set the
|
|
1192
|
+
// header itself was therefore never authenticated — the SDK stripped the
|
|
1193
|
+
// very thing it had just written, on every call, silently. The SDK also
|
|
1194
|
+
// owns the scheme prefix, which is resolved from the API definition
|
|
1195
|
+
// rather than assumed to be \`Bearer\`.
|
|
1196
|
+
if (null != apikey && '' !== apikey) {
|
|
1197
|
+
sdkopts.apikey = apikey
|
|
1198
|
+
}
|
|
1199
|
+
${provider.authBasic ? `
|
|
1200
|
+
// Genuine HTTP Basic Auth needs a SECOND credential (the SDK sends
|
|
1201
|
+
// \`Authorization: Basic base64(apikey:secret)\`) — without it the SDK's
|
|
1202
|
+
// auth stage treats the pair as incomplete and deletes the header, same
|
|
1203
|
+
// as a missing apikey.
|
|
1204
|
+
const secret = res?.keymap?.secret?.value
|
|
1205
|
+
|
|
1206
|
+
if (null != secret && '' !== secret) {
|
|
1207
|
+
sdkopts.secret = secret
|
|
1208
|
+
}
|
|
1209
|
+
` : ''}` : `
|
|
1210
|
+
// This API declares no authentication, so no credential is plumbed. The
|
|
1211
|
+
// SDK's auth stage emits nothing for an auth-inactive model and deletes
|
|
1212
|
+
// any \`authorization\` header regardless of options, so a keymap lookup
|
|
1213
|
+
// here would read a key that could not reach the wire — which is what the
|
|
1214
|
+
// first version of this target did.
|
|
1215
|
+
`}
|
|
1216
|
+
this.shared.sdk = options.test
|
|
1217
|
+
? ${provider.sdkClass}.test(options.testopts || {}, sdkopts)
|
|
1218
|
+
: new ${provider.sdkClass}(sdkopts)
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
exports: {
|
|
1224
|
+
sdk: () => this.shared.sdk,
|
|
1225
|
+
},
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
// Default options.
|
|
1231
|
+
const defaults: ${provider.pluginName}Options = {
|
|
1232
|
+
sdk: {},
|
|
1233
|
+
test: false,
|
|
1234
|
+
testopts: {},
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
Object.assign(${provider.pluginName}, { defaults })
|
|
1238
|
+
|
|
1239
|
+
export default ${provider.pluginName}
|
|
1240
|
+
|
|
1241
|
+
if ('undefined' !== typeof module) {
|
|
1242
|
+
module.exports = ${provider.pluginName}
|
|
1243
|
+
}
|
|
1244
|
+
`)
|
|
1245
|
+
})
|
|
1246
|
+
})
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
// --- src/<Name>Provider-doc.ts ----------------------------------------------
|
|
1251
|
+
|
|
1252
|
+
const ProviderDoc = cmp(function ProviderDoc(props: any) {
|
|
1253
|
+
const { provider } = props
|
|
1254
|
+
|
|
1255
|
+
Folder({ name: 'src' }, () => {
|
|
1256
|
+
File({ name: `${provider.pluginName}-doc.ts` }, () => {
|
|
1257
|
+
Content(`/* Generated by @voxgig/sdkgen. Do not edit. */
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
const messages = {
|
|
1261
|
+
get_info: {
|
|
1262
|
+
desc: 'Get information about the ${provider.api} SDK.',
|
|
1263
|
+
},
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
const sections = {
|
|
1268
|
+
intro: {
|
|
1269
|
+
path: '../provider/doc/intro.md'
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const docs = {
|
|
1274
|
+
sections,
|
|
1275
|
+
messages
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
export default docs
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
if ('undefined' !== typeof module) {
|
|
1282
|
+
module.exports = docs
|
|
1283
|
+
}
|
|
1284
|
+
`)
|
|
1285
|
+
})
|
|
1286
|
+
})
|
|
1287
|
+
})
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
export {
|
|
1291
|
+
Main,
|
|
1292
|
+
recordKey,
|
|
1293
|
+
cmdActions,
|
|
1294
|
+
entityActionList,
|
|
1295
|
+
}
|