@voxgig/sdkgen-infrapack 0.0.12 → 0.0.13
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 +0 -122
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +2 -512
- package/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +0 -21
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -541
- package/package.json +7 -7
- package/sdkgen-package.json +2 -2
|
@@ -21,36 +21,8 @@ import {
|
|
|
21
21
|
import { Gitignore } from './Gitignore_seneca-provider'
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// `ts` SDK.
|
|
27
|
-
//
|
|
28
|
-
// A consumer target in the go-cli / py-data mould — every standard phase is
|
|
29
|
-
// off in model/target/seneca-provider.aon and this component emits the
|
|
30
|
-
// whole package. It differs from those in one way that shapes everything
|
|
31
|
-
// here: it generates into ITS OWN REPO (`output: path`), depends on the SDK
|
|
32
|
-
// as a PUBLISHED npm package rather than by path, and therefore carries a
|
|
33
|
-
// repo's worth of furniture rather than a subfolder's.
|
|
34
|
-
//
|
|
35
|
-
// SHAPE OF THE MAPPING
|
|
36
|
-
//
|
|
37
|
-
// Seneca's store commands are list / load / save / remove. The SDK's are
|
|
38
|
-
// list / load / create / update / remove. `save` is the one that is not
|
|
39
|
-
// one-to-one: Seneca's convention is that an entity carrying an id is an
|
|
40
|
-
// update and one without is a create, so `save` dispatches on `data.id`.
|
|
41
|
-
//
|
|
42
|
-
// Everything else follows from the model:
|
|
43
|
-
// - which cmds exist at all, from the entity's declared ops;
|
|
44
|
-
// - the required path params of each op, which become argument guards (a
|
|
45
|
-
// nested entity like `moon` under `/planet/{planet_id}/moon` cannot build
|
|
46
|
-
// its URL without the parent id, and an opaque 404 from a half-built URL
|
|
47
|
-
// is a bad error message);
|
|
48
|
-
// - the SDK accessor and entity class names, from the same helpers the ts
|
|
49
|
-
// target uses, so the two cannot drift.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Seneca store cmd -> the SDK ops it needs. `save` needs BOTH create and
|
|
53
|
-
// update; it is emitted when either is present and dispatches on the id.
|
|
24
|
+
|
|
25
|
+
|
|
54
26
|
const CMD_OPS: Record<string, string[]> = {
|
|
55
27
|
list: ['list'],
|
|
56
28
|
load: ['load'],
|
|
@@ -59,28 +31,6 @@ const CMD_OPS: Record<string, string[]> = {
|
|
|
59
31
|
}
|
|
60
32
|
|
|
61
33
|
|
|
62
|
-
// The custom ACTIONS one Seneca cmd can reach, as action name -> SDK op.
|
|
63
|
-
//
|
|
64
|
-
// apidef folds a non-CRUD verb into an ordinary op as an extra point marked
|
|
65
|
-
// `select.$action`: GitHub's `PUT /repos/{owner}/{repo}/pulls/{n}/merge` is a
|
|
66
|
-
// second point of `pull.update`, beside the canonical `PATCH`. The SDK
|
|
67
|
-
// selects one with `$action` in the call's argument; without this map the
|
|
68
|
-
// provider has no way to name one at all, and `merge` is simply unreachable
|
|
69
|
-
// through a generated plugin.
|
|
70
|
-
//
|
|
71
|
-
// KEYED BY CMD, NOT BY OP, and that is the whole point of the function.
|
|
72
|
-
// `save` covers create AND update, so an action folded into `create` arrives
|
|
73
|
-
// through `save$` exactly as one folded into `update` does — assuming
|
|
74
|
-
// `update` would send `upload_image` (petstore's
|
|
75
|
-
// `POST /pet/{petId}/uploadImage`, a create point) to the wrong endpoint, and
|
|
76
|
-
// the SDK would then refuse it as an invalid action on an operation the
|
|
77
|
-
// caller never named.
|
|
78
|
-
//
|
|
79
|
-
// A name claimed by an earlier op WINS: `entityActions` walks the op map in
|
|
80
|
-
// sorted-key order, so for `save` that is create before update. Two ops of
|
|
81
|
-
// one entity sharing an action name is not something apidef produces from a
|
|
82
|
-
// spec — the name comes from the route — and if it ever does, a stable choice
|
|
83
|
-
// beats a last-writer-wins one.
|
|
84
34
|
function cmdActions(ent: any, cmd: string): Record<string, string> {
|
|
85
35
|
const ops = CMD_OPS[cmd] || []
|
|
86
36
|
|
|
@@ -107,21 +57,13 @@ function cmdActions(ent: any, cmd: string): Record<string, string> {
|
|
|
107
57
|
}
|
|
108
58
|
|
|
109
59
|
|
|
110
|
-
// The ops that have a route of their OWN, as opposed to nothing but folded-in
|
|
111
|
-
// actions. An op whose every point is an action point has no plain call: a
|
|
112
|
-
// `save$` naming no action still reaches the SDK's `update`, but the SDK finds
|
|
113
|
-
// one point and takes it, so the "canonical" update IS the action's route.
|
|
114
|
-
//
|
|
115
|
-
// Which is what the generated tests need to know before writing a plain,
|
|
116
|
-
// id-bearing save: without this they were emitted for an entity that has no
|
|
117
|
-
// such call to make.
|
|
118
60
|
function canonicalOps(ent: any): string[] {
|
|
119
61
|
return entityOps(ent).filter((opname: string) => {
|
|
120
62
|
const op = (ent.op || {})[opname]
|
|
121
63
|
const points: any[] = (op && op.points) || []
|
|
122
64
|
|
|
123
65
|
return points.some((pt: any) =>
|
|
124
|
-
null == (pt && pt.
|
|
66
|
+
null == (pt && pt.q && pt.q['$action']))
|
|
125
67
|
})
|
|
126
68
|
}
|
|
127
69
|
|
|
@@ -146,9 +88,6 @@ function entityActionList(ent: any):
|
|
|
146
88
|
}
|
|
147
89
|
|
|
148
90
|
|
|
149
|
-
// The required (non-optional) request keys of an op, id first. These are what
|
|
150
|
-
// the SDK needs to build the path, so they are what the provider must have
|
|
151
|
-
// before it calls.
|
|
152
91
|
function requiredKeys(ent: any, opname: string): string[] {
|
|
153
92
|
const idf = entityIdField(ent)
|
|
154
93
|
return opRequestShape(ent, opname).items
|
|
@@ -167,13 +106,6 @@ function addressKeys(ent: any, opname: string): string[] {
|
|
|
167
106
|
const parts = idParts(ent)
|
|
168
107
|
|
|
169
108
|
if (0 < parts.length) {
|
|
170
|
-
// EVERY PART, required or not — for the same reason a single record key
|
|
171
|
-
// always travels. github's api_insights_summary_stat is keyed
|
|
172
|
-
// `actor_type/actor_id` and declares both OPTIONAL, because the op also
|
|
173
|
-
// covers routes that take neither; only `min_timestamp` came through as
|
|
174
|
-
// required. So the call sent the timestamp alone, the split id went
|
|
175
|
-
// nowhere, and every read answered with the same record — the composite
|
|
176
|
-
// half of the defect fixed below for single keys.
|
|
177
109
|
const shape = opRequestShape(ent, opname).items
|
|
178
110
|
for (const part of parts) {
|
|
179
111
|
if (!keys.includes(part) &&
|
|
@@ -194,33 +126,6 @@ function addressKeys(ent: any, opname: string): string[] {
|
|
|
194
126
|
}
|
|
195
127
|
|
|
196
128
|
|
|
197
|
-
// The key that addresses ONE record.
|
|
198
|
-
//
|
|
199
|
-
// `entityIdField` answers whenever the model declares one, which apidef does
|
|
200
|
-
// for any entity carrying a field literally named `id` — and it renames an
|
|
201
|
-
// `<entity>_id` path param to `id` besides, which is why most APIs never reach
|
|
202
|
-
// the fallback.
|
|
203
|
-
//
|
|
204
|
-
// When it does NOT answer, the record key is the LAST path param of the
|
|
205
|
-
// op's own point, in PATH order — a route addresses parents first and the
|
|
206
|
-
// record last, by construction. Without this the key was simply unknown,
|
|
207
|
-
// so a param named `code` failed the `!== idf` test in opParentKeys and
|
|
208
|
-
// was classified as a PARENT — `load$('SAVE20')` threw "coupon load: code
|
|
209
|
-
// is required" instead of loading anything, and the entity was treated as
|
|
210
|
-
// nested throughout.
|
|
211
|
-
//
|
|
212
|
-
// opParams(op) is NOT the source here: it alphabetizes params for output
|
|
213
|
-
// stability, which loses path order on a 3+-param route — Airtable's
|
|
214
|
-
// record (base_id, table_id, record_id) alphabetizes with table_id last,
|
|
215
|
-
// so the old `params[params.length - 1]` picked the wrong parent as the
|
|
216
|
-
// record's own key. The point's own `parts` still has the true order.
|
|
217
|
-
// The path parameters that TOGETHER name one record, when no single one does.
|
|
218
|
-
//
|
|
219
|
-
// apidef sets `id.parts` for an API that addresses a record by several
|
|
220
|
-
// adjacent path parameters — github needs {owner} AND {repo} to name a
|
|
221
|
-
// repository — and `id.sep` (a slash) joins them into the ONE id a Seneca
|
|
222
|
-
// entity carries. Empty for the ordinary single-key entity, which is most of
|
|
223
|
-
// them, so every caller can branch on `0 < parts.length`.
|
|
224
129
|
function idParts(ent: any): string[] {
|
|
225
130
|
const parts = ent?.id?.parts
|
|
226
131
|
return Array.isArray(parts) && 1 < parts.length ?
|
|
@@ -234,18 +139,6 @@ function idSep(ent: any): string {
|
|
|
234
139
|
}
|
|
235
140
|
|
|
236
141
|
|
|
237
|
-
// THE ONE DESCRIPTION OF AN ENTITY'S ID, or null when Seneca's `id` and the
|
|
238
|
-
// API's key already agree and nothing needs translating.
|
|
239
|
-
//
|
|
240
|
-
// It unifies the two cases that used to be handled by separate emitters. A
|
|
241
|
-
// compound key (`{owner}/{repo}`) has several parts; an API that simply calls
|
|
242
|
-
// its key something else (`repo`, `number`) has exactly one. Neither needs a
|
|
243
|
-
// different algorithm — a one-part split is the identity, and a one-part join
|
|
244
|
-
// is the old carry-across — so both come through the same table and the same
|
|
245
|
-
// two functions.
|
|
246
|
-
//
|
|
247
|
-
// `from` is passed through as the model states it: which response field
|
|
248
|
-
// carries each part. apidef derives it and guide.aon can correct it.
|
|
249
142
|
function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string, string> } | null {
|
|
250
143
|
const parts = idParts(ent)
|
|
251
144
|
if (0 < parts.length) {
|
|
@@ -262,8 +155,6 @@ function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string,
|
|
|
262
155
|
return null
|
|
263
156
|
}
|
|
264
157
|
|
|
265
|
-
// A single-key entity whose key is not `id`. `from` defaults to the key's
|
|
266
|
-
// own name, which is what the old carry-across read.
|
|
267
158
|
return { parts: [rk], sep: idSep(ent) }
|
|
268
159
|
}
|
|
269
160
|
|
|
@@ -281,11 +172,8 @@ function recordKey(ent: any): string {
|
|
|
281
172
|
}
|
|
282
173
|
|
|
283
174
|
const canonical = op.points.filter((pt: any) =>
|
|
284
|
-
null == (pt && pt.
|
|
175
|
+
null == (pt && pt.q && pt.q['$action']))
|
|
285
176
|
const point = ownPoint(0 < canonical.length ? canonical : op.points)
|
|
286
|
-
// The LAST variable segment names the record's key. apidef states which
|
|
287
|
-
// segments are variables (its ADR-003), so this reads the name off the
|
|
288
|
-
// vector rather than finding a `{` and slicing the braces back off.
|
|
289
177
|
const vars = pointSegments(point)
|
|
290
178
|
.filter((seg: any) => null != seg.var)
|
|
291
179
|
|
|
@@ -293,12 +181,10 @@ function recordKey(ent: any): string {
|
|
|
293
181
|
return String(vars[vars.length - 1].var)
|
|
294
182
|
}
|
|
295
183
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const query = (point && point.args && point.args.query) || []
|
|
299
|
-
const reqdQuery = query.filter((q: any) => false !== q.reqd)
|
|
184
|
+
const query = (point && point.g && point.g.query) || []
|
|
185
|
+
const reqdQuery = query.filter((q: any) => false !== q.r)
|
|
300
186
|
if (1 === reqdQuery.length) {
|
|
301
|
-
return String(reqdQuery[0].
|
|
187
|
+
return String(reqdQuery[0].n)
|
|
302
188
|
}
|
|
303
189
|
}
|
|
304
190
|
|
|
@@ -306,25 +192,12 @@ function recordKey(ent: any): string {
|
|
|
306
192
|
}
|
|
307
193
|
|
|
308
194
|
|
|
309
|
-
// The guard function's name. Spec-derived param names are not constrained to
|
|
310
|
-
// identifiers — Evervault's `/payments/3ds-sessions/{3ds_session_id}` is the
|
|
311
|
-
// standing example — and a name is a DECLARATION here, so it cannot be
|
|
312
|
-
// bracket-quoted the way a property access can. Non-identifier characters are
|
|
313
|
-
// replaced rather than dropped, so `a-b` and `a_b` cannot collide.
|
|
314
195
|
function guardName(e: any, key: string): string {
|
|
315
196
|
return `need_${e.name}_${String(key).replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
316
197
|
.replace(/^need_(\d)/, 'need__$1')
|
|
317
198
|
}
|
|
318
199
|
|
|
319
200
|
|
|
320
|
-
// The parent PATH params of ONE op: `moon`'s load under
|
|
321
|
-
// `/planet/{planet_id}/moon/{id}` yields ['planet_id']. These are the keys a
|
|
322
|
-
// caller can forget, so each gets a guard.
|
|
323
|
-
//
|
|
324
|
-
// From opParams — the op's declared path params — NOT from opRequestShape.
|
|
325
|
-
// For create/update the latter also returns the request BODY fields, so
|
|
326
|
-
// reading it here generated a "planet name is required" guard for every
|
|
327
|
-
// writable field on every entity.
|
|
328
201
|
function opParentKeys(ent: any, opname: string): string[] {
|
|
329
202
|
const rk = recordKey(ent)
|
|
330
203
|
const op = (ent.op || {})[opname]
|
|
@@ -336,8 +209,8 @@ function opParentKeys(ent: any, opname: string): string[] {
|
|
|
336
209
|
const seen = new Set<string>()
|
|
337
210
|
|
|
338
211
|
for (const p of opParams(op)) {
|
|
339
|
-
const name = String((p as any).
|
|
340
|
-
if (false !== (p as any).
|
|
212
|
+
const name = String((p as any).n)
|
|
213
|
+
if (false !== (p as any).r && name !== rk && name !== 'id') {
|
|
341
214
|
seen.add(name)
|
|
342
215
|
}
|
|
343
216
|
}
|
|
@@ -346,13 +219,6 @@ function opParentKeys(ent: any, opname: string): string[] {
|
|
|
346
219
|
}
|
|
347
220
|
|
|
348
221
|
|
|
349
|
-
// Every parent key the entity has, across its ACTIVE ops — for the seed data
|
|
350
|
-
// and the docs, which describe the entity rather than one call.
|
|
351
|
-
//
|
|
352
|
-
// `entityOps` and not `Object.keys(ent.op)`: an op the model marks
|
|
353
|
-
// `active: false` generates no SDK method, so letting it contribute a key
|
|
354
|
-
// here put a mandatory guard for a parameter of a call that does not exist
|
|
355
|
-
// onto every cmd that does.
|
|
356
222
|
function parentKeys(ent: any): string[] {
|
|
357
223
|
const seen = new Set<string>()
|
|
358
224
|
|
|
@@ -366,20 +232,6 @@ function parentKeys(ent: any): string[] {
|
|
|
366
232
|
}
|
|
367
233
|
|
|
368
234
|
|
|
369
|
-
// A model field's broad shape, for generating seed data that reads as data.
|
|
370
|
-
// The model carries canon strings (`\`$STRING\``), not JS types.
|
|
371
|
-
//
|
|
372
|
-
// ORDER MATTERS and the tests are substring tests, so the container kinds are
|
|
373
|
-
// checked FIRST: a multi-type field's sentinel is the ARRAY
|
|
374
|
-
// `['`$ONE`', [members...]]`, and String() flattens it to a comma-joined
|
|
375
|
-
// string — so a `$ONE` of string|number matched `includes('NUMBER')` and was
|
|
376
|
-
// seeded as a bare number. A union is not a number; it is whatever its first
|
|
377
|
-
// member is, and falling back to a string is the safe answer.
|
|
378
|
-
//
|
|
379
|
-
// `$ARRAY` and `$OBJECT` are in the sentinel vocabulary (see
|
|
380
|
-
// helpers/canonType.ts) and used to fall through to 'string', which put
|
|
381
|
-
// `tags: 'quick-tags'` into test/quick.js — a type-incorrect body that a
|
|
382
|
-
// validating server rejects.
|
|
383
235
|
function fieldKind(type: any): string {
|
|
384
236
|
if (Array.isArray(type)) {
|
|
385
237
|
return 'string'
|
|
@@ -396,21 +248,12 @@ function fieldKind(type: any): string {
|
|
|
396
248
|
}
|
|
397
249
|
|
|
398
250
|
|
|
399
|
-
// Which entity a parent path param refers to: `planet_id` -> `planet`, but
|
|
400
|
-
// only when an entity of that name actually exists. A key that names no
|
|
401
|
-
// entity gets no cross-reference, and the seed falls back to a plain string.
|
|
402
251
|
function parentEntityOf(key: string, names: string[]): string {
|
|
403
252
|
const stem = key.replace(/_id$/, '')
|
|
404
253
|
return names.includes(stem) ? stem : ''
|
|
405
254
|
}
|
|
406
255
|
|
|
407
256
|
|
|
408
|
-
// The repo this provider is released from — NOT the SDK's. A provider is its
|
|
409
|
-
// own package in its own repo, so its manifest's homepage/repository must
|
|
410
|
-
// point there; deriving them from `main: kit: repo` sends every link in the
|
|
411
|
-
// published package to the SDK instead.
|
|
412
|
-
//
|
|
413
|
-
// Order: the project's `output: repo`, else the Seneca convention.
|
|
414
257
|
function providerRepo(model: any, lower: string, tname: string):
|
|
415
258
|
{ url: string, path: string } {
|
|
416
259
|
const host = model?.main?.[KIT]?.repo?.host || 'github.com'
|
|
@@ -437,9 +280,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
437
280
|
const { target, ctx$ } = props
|
|
438
281
|
const { model } = ctx$
|
|
439
282
|
|
|
440
|
-
// HARD REQUIREMENT: this plugin imports the TypeScript SDK. Generating it
|
|
441
|
-
// without `ts` produces a package whose every import fails, so fail at
|
|
442
|
-
// GENERATE time with an actionable message instead.
|
|
443
283
|
const targets = model.main[KIT].target || {}
|
|
444
284
|
if (null == targets.ts) {
|
|
445
285
|
throw new SdkGenError(
|
|
@@ -449,12 +289,12 @@ const Main = cmp(function Main(props: any) {
|
|
|
449
289
|
'then regenerate.')
|
|
450
290
|
}
|
|
451
291
|
|
|
452
|
-
const Name = model.const.Name
|
|
453
|
-
const lower = String(model.const.name)
|
|
454
|
-
const ENV = envName(model)
|
|
455
|
-
const sdkClass = `${Name}SDK`
|
|
456
|
-
const pluginName = `${Name}Provider`
|
|
457
|
-
const fileBase = `${lower}-provider`
|
|
292
|
+
const Name = model.const.Name
|
|
293
|
+
const lower = String(model.const.name)
|
|
294
|
+
const ENV = envName(model)
|
|
295
|
+
const sdkClass = `${Name}SDK`
|
|
296
|
+
const pluginName = `${Name}Provider`
|
|
297
|
+
const fileBase = `${lower}-provider`
|
|
458
298
|
|
|
459
299
|
// The SDK is a PUBLISHED dependency, not a path: this package lives in its
|
|
460
300
|
// own repo. Its name is whatever the ts target publishes under, pin
|
|
@@ -464,45 +304,23 @@ const Main = cmp(function Main(props: any) {
|
|
|
464
304
|
const sdkPkg = packageName(model, 'npm')
|
|
465
305
|
const sdkVersion = packageVersion(model, 'ts')
|
|
466
306
|
|
|
467
|
-
// UNFILTERED by design — see the AGENTS.md sharp edge: entityCollection is
|
|
468
|
-
// the resolver every component must use (getModelPath rebuilds its container
|
|
469
|
-
// per call, defeating the class-name memo), and it deliberately includes
|
|
470
|
-
// inactive entities because the typed-model emitters need them.
|
|
471
|
-
//
|
|
472
|
-
// Which makes filtering `active` the CALLER's job, and this component was
|
|
473
|
-
// the one consumer target that skipped it — go-cli, go-mcp and py-data all
|
|
474
|
-
// re-filter. The cost: MainEntity_ts emits an accessor only for an ACTIVE
|
|
475
|
-
// entity, so an inactive one produced `this.shared.sdk.Ghost()` in the
|
|
476
|
-
// provider and an `assert.equal(typeof sdk.Ghost, 'function')` in its tests,
|
|
477
|
-
// against a method the SDK does not have.
|
|
478
307
|
const entityColl = entityCollection(model)
|
|
479
308
|
|
|
480
309
|
const activeEntities = Object.keys(entityColl).sort()
|
|
481
310
|
.map((key: string) => entityColl[key])
|
|
482
311
|
.filter((ent: any) => false !== ent.active)
|
|
483
312
|
|
|
484
|
-
// Entities this provider can serve: those with at least one op that maps to
|
|
485
|
-
// a Seneca store cmd. An entity with no such op would produce an empty cmd
|
|
486
|
-
// map, which seneca-entity treats as a store that answers nothing.
|
|
487
313
|
const entityNames = activeEntities.map((ent: any) => ent.name)
|
|
488
314
|
|
|
489
315
|
const entities = activeEntities
|
|
490
316
|
.map((ent: any) => {
|
|
491
317
|
const parents = parentKeys(ent)
|
|
492
318
|
|
|
493
|
-
// The parent entity PER KEY. Deriving it from `parents[0]` alone left an
|
|
494
|
-
// entity nested two levels deep with no cross-reference for its outer
|
|
495
|
-
// parents, so their seed values came out as the literal '0'.
|
|
496
319
|
const parentOf: Record<string, string> = {}
|
|
497
320
|
for (const key of parents) {
|
|
498
321
|
parentOf[key] = parentEntityOf(key, entityNames)
|
|
499
322
|
}
|
|
500
323
|
|
|
501
|
-
// The parent keys of each op SEPARATELY. The guards used to be the union
|
|
502
|
-
// across all ops, applied to every cmd alike, while the argument handed
|
|
503
|
-
// to the SDK was computed per op — so an entity whose routes are not
|
|
504
|
-
// uniformly nested (a flat `load`, a nested `create`) demanded a
|
|
505
|
-
// parameter its own call would never use.
|
|
506
324
|
const opParents: Record<string, string[]> = {}
|
|
507
325
|
for (const opname of entityOps(ent)) {
|
|
508
326
|
opParents[opname] = opParentKeys(ent, opname)
|
|
@@ -511,27 +329,11 @@ const Main = cmp(function Main(props: any) {
|
|
|
511
329
|
return {
|
|
512
330
|
ent,
|
|
513
331
|
name: ent.name,
|
|
514
|
-
// The SDK ACCESSOR on the client (`client.Moon()`), which is the
|
|
515
|
-
// entity's PascalCase name — NOT entityClassName, which is the
|
|
516
|
-
// collision-safe CLASS name the accessor constructs (`MoonEntity`).
|
|
517
|
-
// Calling the class name reads plausibly and fails at runtime with
|
|
518
|
-
// "sdk.MoonEntity is not a function". MainEntity_ts is the authority
|
|
519
|
-
// for this: it declares the method as `${entity.Name}()`.
|
|
520
332
|
acc: ent.Name,
|
|
521
|
-
// The entity's canonical route, used to probe the live server for
|
|
522
|
-
// liveness. Path params are left in place only if the route has
|
|
523
|
-
// them — a collection route (the `list` op's) has none, which is why
|
|
524
|
-
// entityPath prefers it.
|
|
525
333
|
path: entityPath(ent),
|
|
526
334
|
cls: entityClassName(ent, entityColl),
|
|
527
335
|
ops: entityOps(ent),
|
|
528
336
|
idf: entityIdField(ent),
|
|
529
|
-
// The field this API's routes actually ADDRESS a record by, which is
|
|
530
|
-
// not always `id` and is not always what entityIdField answers (that
|
|
531
|
-
// returns null when the load match has no `id`, leaving recordKey to
|
|
532
|
-
// read the route's last variable segment). The doc and test emitters
|
|
533
|
-
// in Extras need the same answer the handler emitters use, or the
|
|
534
|
-
// seed they build is keyed by a field the routes never look at.
|
|
535
337
|
rk: recordKey(ent),
|
|
536
338
|
// The composite key, when this API addresses a record by several
|
|
537
339
|
// path params at once. The doc and test emitters in Extras need the
|
|
@@ -540,41 +342,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
540
342
|
// rejects.
|
|
541
343
|
idparts: idParts(ent),
|
|
542
344
|
idsep: idSep(ent),
|
|
543
|
-
// DOES EACH SINGLE-RECORD OP ACTUALLY CARRY THE RECORD'S KEY?
|
|
544
|
-
//
|
|
545
|
-
// Only then can a wrong id miss on a LOAD: github's `interaction`
|
|
546
|
-
// reads `/user/interaction-limits` — a singleton, called as
|
|
547
|
-
// `load({})` — so `load$('no-such-id')` correctly returns the one
|
|
548
|
-
// record there is, and a not-found test against it asserts the
|
|
549
|
-
// opposite of the truth.
|
|
550
|
-
//
|
|
551
|
-
// And only then can a REMOVE delete what a create just made. A
|
|
552
|
-
// tag-bucket entity can have ops addressing different resources
|
|
553
|
-
// entirely: github's `action` is keyed `archive_format` from its
|
|
554
|
-
// download route while its remove takes `hosted_runner_id` and
|
|
555
|
-
// `org_id`, so the remove addressed by parent scope alone — it
|
|
556
|
-
// deleted whichever record the store happened to yield first, which
|
|
557
|
-
// was usually a SEEDED one, and the round-trip failed on the record
|
|
558
|
-
// it had created surviving. Intermittently: the created record's id
|
|
559
|
-
// is random, so where it falls in iteration order decides.
|
|
560
|
-
//
|
|
561
|
-
// Parent keys alone do not distinguish records, which is why this
|
|
562
|
-
// asks for the record's key or every composite part rather than
|
|
563
|
-
// merely for "the route has a parameter".
|
|
564
|
-
// WHICH SINGLE-RECORD OPS ADDRESS A DIFFERENT RESOURCE ENTIRELY.
|
|
565
|
-
//
|
|
566
|
-
// Not merely "cannot address the record": an op that addresses
|
|
567
|
-
// NOTHING is a singleton read, and github's `interaction`
|
|
568
|
-
// (`/user/interaction-limits`) is a real one. This is the other case
|
|
569
|
-
// — the op addresses something, and it is not this record. Every one
|
|
570
|
-
// is a tag-derived entity whose ops were gathered from unrelated
|
|
571
|
-
// routes: `migration`'s remove takes `owner` and `repo` and deletes
|
|
572
|
-
// a repository's migration archive, `user`'s deletes a GPG KEY, and
|
|
573
|
-
// `pull`'s deletes a review COMMENT. The id the caller passed is
|
|
574
|
-
// dropped, and the request goes anyway.
|
|
575
|
-
//
|
|
576
|
-
// Reads are untouched across the whole of github — this is 7 removes
|
|
577
|
-
// and 5 updates, and both are writes.
|
|
578
345
|
idmisaddressed: ['remove', 'update'].reduce(
|
|
579
346
|
(acc: Record<string, boolean>, opname: string) => {
|
|
580
347
|
acc[opname] = null != (ent.op || {})[opname] &&
|
|
@@ -594,8 +361,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
594
361
|
keys.includes(recordKey(ent))
|
|
595
362
|
return acc
|
|
596
363
|
}, {}),
|
|
597
|
-
// Where each part is carried in a response. The test emitter needs
|
|
598
|
-
// it to know whether a created record's id can be rebuilt at all.
|
|
599
364
|
idfrom: (ent?.id?.from) || {},
|
|
600
365
|
parents,
|
|
601
366
|
parentOf,
|
|
@@ -613,30 +378,15 @@ const Main = cmp(function Main(props: any) {
|
|
|
613
378
|
acc[cmd] = cmdActions(ent, cmd)
|
|
614
379
|
return acc
|
|
615
380
|
}, {}),
|
|
616
|
-
// The same information flattened, for the README and the generated
|
|
617
|
-
// tests: [{ cmd, op, action, path }].
|
|
618
381
|
actionList: entityActionList(ent),
|
|
619
|
-
// The ops with a route of their own. `ops` minus those that are
|
|
620
|
-
// nothing but folded-in actions — what the generated tests consult
|
|
621
|
-
// before assuming a plain call exists.
|
|
622
382
|
canonicalOps: canonicalOps(ent),
|
|
623
|
-
// Required fields only: a seed record has to satisfy the shape the
|
|
624
|
-
// SDK will hand back, and optional noise makes the assertions
|
|
625
|
-
// harder to read.
|
|
626
|
-
//
|
|
627
|
-
// A parent path param is then FORCED IN even when the entity's own
|
|
628
|
-
// schema omits it or marks it optional. A path param is a routing key,
|
|
629
|
-
// not necessarily a response field: when the child's schema left it
|
|
630
|
-
// out the seeded record had no link back to its parent, and the mock's
|
|
631
|
-
// match found nothing — which the nested `load` test reported as a
|
|
632
|
-
// TypeError and the nested `list` test reported as a pass.
|
|
633
383
|
fields: (() => {
|
|
634
|
-
const req = (ent.fields ||
|
|
635
|
-
.filter((f: any) => false !== f.
|
|
384
|
+
const req = (Object.values(ent.fields || {}))
|
|
385
|
+
.filter((f: any) => false !== f.r)
|
|
636
386
|
.map((f: any) => ({
|
|
637
|
-
name: f.
|
|
638
|
-
kind: fieldKind(f.
|
|
639
|
-
parentEntity: parentEntityOf(f.
|
|
387
|
+
name: f.n,
|
|
388
|
+
kind: fieldKind(f.t),
|
|
389
|
+
parentEntity: parentEntityOf(f.n, entityNames),
|
|
640
390
|
}))
|
|
641
391
|
|
|
642
392
|
const have = new Set(req.map((f: any) => f.name))
|
|
@@ -677,27 +427,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
677
427
|
// once by the external pass — see cmp/ExternalTarget.
|
|
678
428
|
const sdkrel = ctx$.sdkrelpath || '..'
|
|
679
429
|
|
|
680
|
-
// WHERE THE SDK SOURCE IS, AND WHY IT IS NOT `sdkrel`.
|
|
681
|
-
//
|
|
682
|
-
// `sdkrel` is a walk back through the FILESYSTEM, and it is committed here
|
|
683
|
-
// — into the docs, the live-test headers and the develop-locally recipe.
|
|
684
|
-
// That makes generated content depend on one machine's directory layout.
|
|
685
|
-
// voxgig-solardemo-sdk committed '../../voxgig-sdk/voxgig-solardemo-sdk',
|
|
686
|
-
// where `voxgig-sdk` is the name of a workspace directory on one laptop and
|
|
687
|
-
// no part of any model; a second developer with both repos under a
|
|
688
|
-
// differently named parent regenerates a diff in tracked files and follows
|
|
689
|
-
// instructions that are wrong on one of the two machines.
|
|
690
|
-
//
|
|
691
|
-
// So the SDK is named by its PIN instead — repository and release tag, both
|
|
692
|
-
// facts about the SDK rather than about anyone's disk — and fetched to a
|
|
693
|
-
// fixed path INSIDE this repo. `make sdk-src` puts it there. The path is
|
|
694
|
-
// then the same string on every machine and under every layout, which is
|
|
695
|
-
// what lets this repo regenerate itself from a checkout it fetched (see
|
|
696
|
-
// SdkPin and the Makefile).
|
|
697
|
-
//
|
|
698
|
-
// The tag is `v<version>`: the SDK's publish workflow cuts exactly that for
|
|
699
|
-
// its primary npm target, so the pin needs no separate bookkeeping and
|
|
700
|
-
// cannot drift from the dependency version.
|
|
701
430
|
const sdkRepoUrl = String(repoInfo(model).repoUrl || '')
|
|
702
431
|
const sdkTag = 'v' + sdkVersion
|
|
703
432
|
const sdkRepoDir = sdkRepoUrl.replace(/[/]+$/, '').split('/').pop() || 'sdk'
|
|
@@ -708,22 +437,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
708
437
|
const sdkPinned = '' !== sdkRepoUrl
|
|
709
438
|
const sdkSrc = sdkPinned ? SDK_SRC_DIR + '/' + sdkRepoDir : sdkrel
|
|
710
439
|
|
|
711
|
-
// Where a live run points — and whether there is anything honest to point
|
|
712
|
-
// it at.
|
|
713
|
-
//
|
|
714
|
-
// NOT simply `servers[0].url`. For an OpenAPI-derived model that is the
|
|
715
|
-
// PRODUCTION host of a third-party API, and everything gated on it aims
|
|
716
|
-
// there: the `describe('live')` block, which `npm test` runs on every CI
|
|
717
|
-
// push on three operating systems; the `serverUp` probe in front of it; and
|
|
718
|
-
// test/quick.js, whose header says "start the companion server first" while
|
|
719
|
-
// BASE silently defaults to production and whose body is a create / update /
|
|
720
|
-
// remove cycle. A live suite that reaches a stranger's API from CI is not a
|
|
721
|
-
// live suite, it is traffic — and unauthenticated traffic at that.
|
|
722
|
-
//
|
|
723
|
-
// So a live base is taken only when it is unambiguously OURS: declared
|
|
724
|
-
// outright by the project, or a loopback address, which no third party can
|
|
725
|
-
// be behind. Anything else leaves live testing ungenerated, which is the
|
|
726
|
-
// honest answer for an API nobody here runs.
|
|
727
440
|
const live = model?.main?.[KIT]?.test?.live || {}
|
|
728
441
|
const servers = (model?.main?.[KIT]?.info?.servers || [])
|
|
729
442
|
const specBase = 0 < servers.length ? String(servers[0].url || '') : ''
|
|
@@ -748,9 +461,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
748
461
|
const provider = {
|
|
749
462
|
Name, lower, ENV, sdkClass, pluginName, fileBase,
|
|
750
463
|
sdkPkg, sdkVersion, entities,
|
|
751
|
-
// The SDK dependency, resolved ONCE. PackageJson emits it and the CI
|
|
752
|
-
// note describes it, and computing it twice is how the second caller
|
|
753
|
-
// came to pass a half-built provider object into it.
|
|
754
464
|
sdkDep,
|
|
755
465
|
// Whether that dependency comes from outside a registry. The generated
|
|
756
466
|
// CI note says so, because "npm install is all you need" stops being
|
|
@@ -761,60 +471,28 @@ const Main = cmp(function Main(props: any) {
|
|
|
761
471
|
// that says the wrong one is worse than no note.
|
|
762
472
|
sdkDepKind: sdkDep.startsWith('github:') ? 'git' :
|
|
763
473
|
(sdkDep.startsWith('http') ? 'release' : 'npm'),
|
|
764
|
-
// THE OPT-IN A NON-REGISTRY DEPENDENCY NEEDS, or ''.
|
|
765
|
-
//
|
|
766
|
-
// npm 12 defaults `allow-git` and `allow-remote` to "none" and REFUSES
|
|
767
|
-
// such a dependency outright — EALLOWGIT / EALLOWREMOTE, before it
|
|
768
|
-
// fetches anything. Older npm allowed both, so a workflow that passes
|
|
769
|
-
// today fails the moment its runner image updates; this provider's own
|
|
770
|
-
// publish run failed that way within hours of the build run passing,
|
|
771
|
-
// because publishing upgrades npm first.
|
|
772
|
-
//
|
|
773
|
-
// Emitted ONLY where the project chose such a dependency: it is the
|
|
774
|
-
// project opting into what it already declared, not a default weakened
|
|
775
|
-
// for everyone. Publishing the SDK remains the durable answer, and this
|
|
776
|
-
// flag exists so an unpublished one is workable meanwhile.
|
|
777
474
|
sdkInstallFlag: sdkDep.startsWith('github:') ? ' --allow-git=all' :
|
|
778
475
|
(sdkDep.startsWith('http') ? ' --allow-remote=all' : ''),
|
|
779
476
|
repoUrl: repo.url,
|
|
780
|
-
// The SDK's own repo, for pointing at the companion test server which is
|
|
781
|
-
// only distributed in source.
|
|
782
477
|
sdkRepoUrl,
|
|
783
478
|
sdkTag,
|
|
784
479
|
sdkRepoDir,
|
|
785
480
|
sdkPinned,
|
|
786
|
-
// Where the SDK SOURCE is reached, as every generated file spells it.
|
|
787
|
-
// Layout-independent when the SDK has a repo to pin. See above.
|
|
788
481
|
sdkSrc,
|
|
789
482
|
api: apiName(model),
|
|
790
483
|
version: packageVersion(model, target.name),
|
|
791
484
|
liveBase,
|
|
792
485
|
liveApp,
|
|
793
|
-
// The sponsor line. @seneca/maintain's `content_readme` check requires
|
|
794
|
-
// the publisher's name in the README, so this is load-bearing rather
|
|
795
|
-
// than decorative — the generated `maintain` test fails without it.
|
|
796
486
|
publisher: PUBLISHER,
|
|
797
487
|
publisherUrl: PUBLISHER_URL,
|
|
798
|
-
// A route with NO path params, so the probe is a plain GET. An entity
|
|
799
|
-
// whose every route is parameterised gives none, and then the probe
|
|
800
|
-
// falls back to the base URL itself.
|
|
801
488
|
probePath: (entities.find((e: any) =>
|
|
802
489
|
'' !== e.path && !e.path.includes('{')) || { path: '' }).path,
|
|
803
|
-
// The provider's OWN published name. Not derived from the SDK slug the
|
|
804
|
-
// way a language target's is — a provider is `@seneca/<name>-provider` —
|
|
805
|
-
// so read the project's pin directly and default to that shape.
|
|
806
490
|
pkgName: providerPackage(model, lower, target.name),
|
|
807
491
|
// Whether this API authenticates at all. Decides if the plugin plumbs a
|
|
808
492
|
// credential: the SDK's auth stage emits nothing for an auth-inactive
|
|
809
493
|
// model and strips the authorization header regardless of options, so
|
|
810
494
|
// plumbing one anyway produces a credential path that cannot work.
|
|
811
495
|
authActive: isAuthActive(model),
|
|
812
|
-
// Whether this API's scheme is genuine HTTP Basic Auth (two credentials,
|
|
813
|
-
// base64-joined), matching the SDK's own auth.basic signal. Decides
|
|
814
|
-
// whether the plugin also plumbs a `secret` alongside `apikey` — a
|
|
815
|
-
// Basic-Auth SDK's own auth stage deletes the header when either is
|
|
816
|
-
// missing, so a provider forwarding apikey alone could never
|
|
817
|
-
// authenticate, the same class of gap `apikey`-only forwarding was.
|
|
818
496
|
authBasic: isAuthActive(model) && isHttpBasicAuth(model),
|
|
819
497
|
}
|
|
820
498
|
|
|
@@ -824,8 +502,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
824
502
|
// target calls its own.
|
|
825
503
|
Gitignore({})
|
|
826
504
|
|
|
827
|
-
// Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, tsfmt.json and
|
|
828
|
-
// both tsconfigs. Same for every provider.
|
|
829
505
|
Copy({
|
|
830
506
|
from: 'tm/' + target.name,
|
|
831
507
|
replace: { ...ctx$.stdrep },
|
|
@@ -843,36 +519,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
843
519
|
})
|
|
844
520
|
|
|
845
521
|
|
|
846
|
-
// HOW THE PROVIDER DEPENDS ON THE SDK IT WRAPS, as one dependency value.
|
|
847
|
-
//
|
|
848
|
-
// Default: the PUBLISHED package pinned to the version the `ts` target
|
|
849
|
-
// publishes, so the two can never disagree. Right whenever the SDK is on a
|
|
850
|
-
// registry — and wrong when it is not. An SDK for a private API, or one not
|
|
851
|
-
// published yet, leaves the provider unable to `npm install` at all: the
|
|
852
|
-
// dependency 404s, so the package cannot be built, tested or released. That
|
|
853
|
-
// is not hypothetical; it is why @seneca/github-provider could not be
|
|
854
|
-
// regenerated and released for weeks.
|
|
855
|
-
//
|
|
856
|
-
// `kind: 'git'` points at a GIT TAG instead, which needs no registry.
|
|
857
|
-
//
|
|
858
|
-
// NPM RESOLVES A GIT DEPENDENCY AGAINST THE REPOSITORY ROOT, and sdkgen
|
|
859
|
-
// generates the TypeScript SDK into `ts/`. So `kind: 'git'` suits an SDK
|
|
860
|
-
// whose package.json IS the repository root, and NOT the layout this
|
|
861
|
-
// toolchain produces.
|
|
862
|
-
//
|
|
863
|
-
// THE `::path:` SUBDIRECTORY SYNTAX DOES NOT WORK, and it looks like it
|
|
864
|
-
// does. npm-package-arg parses `#<ref>::path:ts` and reports
|
|
865
|
-
// `gitSubdir: /ts`, so a spec built that way reads as correct — but the
|
|
866
|
-
// INSTALLER ignores it: npm clones the repository and opens package.json at
|
|
867
|
-
// the clone root, failing with ENOENT on linux, macOS and Windows alike.
|
|
868
|
-
// That was measured, on all three, after the parser had said otherwise.
|
|
869
|
-
//
|
|
870
|
-
// `kind: 'release'` is the form that works for a package in a subdirectory:
|
|
871
|
-
// a GitHub release asset, which is `npm pack` output attached to the tag.
|
|
872
|
-
// npm installs an https tarball natively and never looks at the repository
|
|
873
|
-
// layout at all.
|
|
874
|
-
//
|
|
875
|
-
// `spec` still wins over all of it, for anything the shorthand cannot say.
|
|
876
522
|
function sdkDependency(model: any, target: any, provider: any): string {
|
|
877
523
|
const dep = model?.main?.[KIT]?.target?.[target.name]?.sdk?.dep || {}
|
|
878
524
|
|
|
@@ -913,8 +559,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
|
|
|
913
559
|
}
|
|
914
560
|
|
|
915
561
|
if ('release' === kind) {
|
|
916
|
-
// The asset `npm pack` produces: scope and name flattened, then the
|
|
917
|
-
// version. `asset` overrides it for a project that names its own.
|
|
918
562
|
const asset = String(dep.asset || '').trim() || (
|
|
919
563
|
provider.sdkPkg.replace(/^@/, '').replace(/\//g, '-') +
|
|
920
564
|
'-' + provider.sdkVersion + '.tgz')
|
|
@@ -926,7 +570,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
|
|
|
926
570
|
}
|
|
927
571
|
|
|
928
572
|
|
|
929
|
-
// --- package.json -----------------------------------------------------------
|
|
930
573
|
|
|
931
574
|
const PackageJson = cmp(function PackageJson(props: any) {
|
|
932
575
|
const { provider, target } = props
|
|
@@ -934,19 +577,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
934
577
|
|
|
935
578
|
const deps = collectDeps(model, target.name, target.deps, props.ctx$.log)
|
|
936
579
|
|
|
937
|
-
// collectDeps returns { name, version, source, raw } — the DECLARED kind
|
|
938
|
-
// (prod/peer/dev) is on `raw`, not hoisted. Reading `d.kind` instead
|
|
939
|
-
// silently yields an empty manifest section, which is how the first cut of
|
|
940
|
-
// this component shipped a package.json with no seneca peers at all.
|
|
941
|
-
//
|
|
942
|
-
// `kind` is a COMMA-SEPARATED LIST, so one dependency can land in more than
|
|
943
|
-
// one manifest section. That is the Seneca plugin convention and not an
|
|
944
|
-
// embellishment: `seneca` is a PEER (the plugin must run inside the host's
|
|
945
|
-
// instance, never its own bundled copy) and also a DEV dependency (the test
|
|
946
|
-
// suite does `require('seneca')` directly, so a bare `npm install` in a
|
|
947
|
-
// clean checkout has to produce it). collectDeps deduplicates by package
|
|
948
|
-
// name and the model's dep map is keyed by name, so the same package cannot
|
|
949
|
-
// be declared twice — the kind has to carry the list instead.
|
|
950
580
|
const dep = (kind: string) => {
|
|
951
581
|
const out: Record<string, string> = {}
|
|
952
582
|
const kinds = (d: any) => String((d.raw && d.raw.kind) || '')
|
|
@@ -978,9 +608,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
978
608
|
keywords: ['seneca', provider.lower, `${provider.lower}-provider`,
|
|
979
609
|
provider.publisher.toLowerCase(), 'sdk'],
|
|
980
610
|
author,
|
|
981
|
-
// Omitted entirely when the project names none, rather than emitted as an
|
|
982
|
-
// empty array — npm treats `"contributors": []` as a declaration that
|
|
983
|
-
// there are none, which is a different claim from not having said.
|
|
984
611
|
...(0 < contributors.length ? { contributors } : {}),
|
|
985
612
|
license: 'MIT',
|
|
986
613
|
repository: { type: 'git', url: `git+${provider.repoUrl}.git` },
|
|
@@ -1012,17 +639,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
1012
639
|
'npm run build && npm run test && npm run repo-tag && ' +
|
|
1013
640
|
'npm publish --access public --registry https://registry.npmjs.org',
|
|
1014
641
|
},
|
|
1015
|
-
// What actually ships. Without `files`, `npm publish` packs the test
|
|
1016
|
-
// suite and build output into the tarball.
|
|
1017
|
-
//
|
|
1018
|
-
// `doc` IS PART OF THE PACKAGE. The generated README links to
|
|
1019
|
-
// doc/tutorial.md, doc/how-to.md, doc/reference.md and
|
|
1020
|
-
// doc/explanation.md with relative paths, so omitting it published a
|
|
1021
|
-
// README whose every documentation link 404s for anyone reading the
|
|
1022
|
-
// installed package rather than the repository. Either the links become
|
|
1023
|
-
// absolute repository URLs or the files ship; they ship, because the
|
|
1024
|
-
// docs describe the exact version installed and a URL would drift to
|
|
1025
|
-
// whatever main says later.
|
|
1026
642
|
files: ['dist', 'doc', 'src/**/*.ts', 'LICENSE'],
|
|
1027
643
|
engines: { node: '>=24' },
|
|
1028
644
|
dependencies: {
|
|
@@ -1042,7 +658,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
1042
658
|
})
|
|
1043
659
|
|
|
1044
660
|
|
|
1045
|
-
// --- src/<name>-provider.ts -------------------------------------------------
|
|
1046
661
|
|
|
1047
662
|
const ProviderSource = cmp(function ProviderSource(props: any) {
|
|
1048
663
|
const { provider } = props
|
|
@@ -1212,26 +827,6 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1212
827
|
})
|
|
1213
828
|
}
|
|
1214
829
|
|
|
1215
|
-
// Seneca's own entity key is ALWAYS literally `id` — `load$('x')` sets
|
|
1216
|
-
// `q = { id: 'x' }`, and the id of the entity it builds comes off
|
|
1217
|
-
// `data.id`. An API that addresses a record by anything else therefore
|
|
1218
|
-
// needs translating in both directions, or `load$` requests a record
|
|
1219
|
-
// keyed `undefined` and every entity handed back has no id at all — one
|
|
1220
|
-
// that cannot then be saved or removed.
|
|
1221
|
-
// ID TRANSLATION, ONE ALGORITHM AND A TABLE.
|
|
1222
|
-
//
|
|
1223
|
-
// Seneca entities carry exactly one `id`. Plenty of APIs do not: they
|
|
1224
|
-
// key a record by a differently-named field (`repo`, `number`), or by
|
|
1225
|
-
// SEVERAL path parameters at once with no single one that is the id
|
|
1226
|
-
// (github's `/repos/{owner}/{repo}`). Both need translating in both
|
|
1227
|
-
// directions, or `load$` asks for a record keyed `undefined` and every
|
|
1228
|
-
// record handed back has no id to save or remove it by.
|
|
1229
|
-
//
|
|
1230
|
-
// The LOGIC is the same for every API; only which parameters, which
|
|
1231
|
-
// separator and where they live in a response differ, and those are
|
|
1232
|
-
// model data. So this emits one `splitid`, one `joinid` and a table —
|
|
1233
|
-
// not a bespoke pair of functions per entity, which is what it used to
|
|
1234
|
-
// do and which duplicated the same algorithm N times over.
|
|
1235
830
|
const translated = provider.entities.filter((e: any) => null != idSpec(e.ent))
|
|
1236
831
|
|
|
1237
832
|
if (0 < translated.length) {
|
|
@@ -1349,14 +944,6 @@ ${rows}
|
|
|
1349
944
|
`)
|
|
1350
945
|
}
|
|
1351
946
|
|
|
1352
|
-
// WHICH SDK OP SERVES EACH `action$`, per entity and per cmd.
|
|
1353
|
-
//
|
|
1354
|
-
// Emitted for EVERY cmd, including the ones with no actions at all.
|
|
1355
|
-
// That empty map is not waste: it is what lets `actionop` refuse an
|
|
1356
|
-
// `action$` on an entity that has none, instead of ignoring the key
|
|
1357
|
-
// and performing an ordinary call. Passing `action$` and getting a
|
|
1358
|
-
// plain save is the failure this whole mechanism exists to prevent —
|
|
1359
|
-
// it is how GitHub's `merge` silently became an "update".
|
|
1360
947
|
Content(` // The custom actions each cmd can reach, as action -> SDK op. An action
|
|
1361
948
|
// is an alternative POINT of an ordinary op (\`select.$action\` in the API
|
|
1362
949
|
// model), so \`save$\` routes by this map rather than assuming update: an
|
|
@@ -1370,12 +957,6 @@ ${rows}
|
|
|
1370
957
|
const name = String(cmd.val$ ?? cmd)
|
|
1371
958
|
const map = e.actions[name] || {}
|
|
1372
959
|
const names = Object.keys(map).sort()
|
|
1373
|
-
// COMPUTED KEYS, not `jsKey`. An action named `__proto__` written
|
|
1374
|
-
// as a plain (or quoted) object-literal key SETS THE PROTOTYPE
|
|
1375
|
-
// instead of creating a property, so the action would vanish from
|
|
1376
|
-
// its own map and be unreachable. A computed key always defines an
|
|
1377
|
-
// own property. apidef derives an action name from a route segment,
|
|
1378
|
-
// and `__proto__` is a legal one.
|
|
1379
960
|
Content(` ${name}: {${names.map((a: string) =>
|
|
1380
961
|
` [${JSON.stringify(a)}]: '${map[a]}'`).join(',')}${0 < names.length ? ' ' : ''}},
|
|
1381
962
|
`)
|
|
@@ -1450,8 +1031,6 @@ ${rows}
|
|
|
1450
1031
|
|
|
1451
1032
|
`)
|
|
1452
1033
|
|
|
1453
|
-
// The cmd map, declared up front so every action is attached to a
|
|
1454
|
-
// shape seneca-entity can read before the actions are defined.
|
|
1455
1034
|
Content(` const entity: any = {
|
|
1456
1035
|
`)
|
|
1457
1036
|
each(provider.entities, (e: any) => {
|
|
@@ -1472,22 +1051,7 @@ ${rows}
|
|
|
1472
1051
|
`)
|
|
1473
1052
|
|
|
1474
1053
|
each(provider.entities, (e: any) => {
|
|
1475
|
-
// The guard set is PER OP, not the union across the entity's ops. An
|
|
1476
|
-
// entity whose routes are not uniformly nested — a flat `load`, a
|
|
1477
|
-
// nested `create` — used to demand the parent id on the flat call too,
|
|
1478
|
-
// an argument its own SDK request would then never use.
|
|
1479
|
-
//
|
|
1480
|
-
// `save` guards the union of create's and update's keys: one action
|
|
1481
|
-
// serves both and dispatches at runtime, so it cannot know which set
|
|
1482
|
-
// applies until it has the data.
|
|
1483
|
-
// A COMPOSITE-KEY ENTITY GUARDS NOTHING SEPARATELY. Its parent keys
|
|
1484
|
-
// travel INSIDE the id, so demanding `q.owner` as well would reject
|
|
1485
|
-
// `load$('octocat/hello-world')` — the very call the composite id
|
|
1486
|
-
// exists to allow. splitid() does the checking instead, and refuses
|
|
1487
|
-
// a wrong part count by name.
|
|
1488
1054
|
const eparts = idParts(e.ent)
|
|
1489
|
-
// The whole id description for this entity, or null when none is
|
|
1490
|
-
// needed. `out` branches on it rather than on the record key.
|
|
1491
1055
|
const espec = idSpec(e.ent)
|
|
1492
1056
|
const guard = (cmd: string, src: string) => {
|
|
1493
1057
|
const keys = 'save' === cmd ?
|
|
@@ -1501,12 +1065,6 @@ ${rows}
|
|
|
1501
1065
|
.join('')
|
|
1502
1066
|
}
|
|
1503
1067
|
|
|
1504
|
-
// THE REFUSAL LINE, for a cmd whose only route addresses a
|
|
1505
|
-
// different resource. Placed AFTER the action branch — an action
|
|
1506
|
-
// route is named explicitly and is exactly how the real operation is
|
|
1507
|
-
// reached — and BEFORE the guards, so the caller is told the cmd
|
|
1508
|
-
// does not exist for this entity rather than being asked for a
|
|
1509
|
-
// parameter that would not have helped.
|
|
1510
1068
|
const refuse = (cmd: string) => {
|
|
1511
1069
|
if (true !== (e.idmisaddressed || {})[cmd]) {
|
|
1512
1070
|
return ''
|
|
@@ -1520,26 +1078,8 @@ ${rows}
|
|
|
1520
1078
|
`
|
|
1521
1079
|
}
|
|
1522
1080
|
|
|
1523
|
-
// Reading the record's own key off the Seneca query, which always
|
|
1524
|
-
// spells it `id`, and every other required key off its own name.
|
|
1525
1081
|
const rk = recordKey(e.ent)
|
|
1526
1082
|
|
|
1527
|
-
// For a composite key the parameters come out of the split, which the
|
|
1528
|
-
// handler binds to `key` before the call. Any required key the id
|
|
1529
|
-
// does NOT carry still comes off the query.
|
|
1530
|
-
// THE RECORD'S OWN KEY IS ALWAYS SENT, whether or not the match
|
|
1531
|
-
// declares it required.
|
|
1532
|
-
//
|
|
1533
|
-
// An op gathers several routes, and a parameter only one of them
|
|
1534
|
-
// uses comes through OPTIONAL: github's IssueLoadMatch has `owner`
|
|
1535
|
-
// and `repo` required but `id` optional, because the op also covers
|
|
1536
|
-
// `/repos/{owner}/{repo}/issues/comments/{comment_id}`. Sending only
|
|
1537
|
-
// the required keys called `Issue().load({owner, repo})` — the id
|
|
1538
|
-
// the caller passed to `load$` went nowhere, so every read of any
|
|
1539
|
-
// issue in that repo answered with the same record and
|
|
1540
|
-
// `load$('no-such-issue')` returned one. Ten entities failed their
|
|
1541
|
-
// not-found test on it, and the ones that "passed" were passing for
|
|
1542
|
-
// the wrong reason. addressKeys is what the test emitter reads too.
|
|
1543
1083
|
const sdkArg = (opname: string) => {
|
|
1544
1084
|
const keys = addressKeys(e.ent, opname)
|
|
1545
1085
|
if (0 === keys.length) {
|
|
@@ -1571,24 +1111,6 @@ ${rows}
|
|
|
1571
1111
|
null == espec ? `plain(${expr})` :
|
|
1572
1112
|
`joinid('${e.name}', plain(${expr})${null == vals ? '' : ', ' + vals})`
|
|
1573
1113
|
|
|
1574
|
-
// The action branch, emitted for every cmd whether or not this entity
|
|
1575
|
-
// has actions. `actionop` is what refuses an unknown name, so leaving
|
|
1576
|
-
// it out where the map is empty would restore the silent drop for
|
|
1577
|
-
// exactly the entities most likely to be typed at by mistake.
|
|
1578
|
-
//
|
|
1579
|
-
// IT COMES BEFORE THE PARENT GUARDS, and that ordering is the whole
|
|
1580
|
-
// of its correctness. The guards describe the CANONICAL route —
|
|
1581
|
-
// opParams drops action points when computing them — and an action
|
|
1582
|
-
// route need not be nested the same way: Zoom's canonical update is
|
|
1583
|
-
// `/user/{user_id}/meeting/{id}` while its status action hangs off
|
|
1584
|
-
// `/meeting/{id}`. Guarding first rejected that action for want of a
|
|
1585
|
-
// `user_id` its own URL has no segment for.
|
|
1586
|
-
//
|
|
1587
|
-
// The action path is not left unguarded, it is guarded by the RIGHT
|
|
1588
|
-
// thing: the SDK builds the action's own point and refuses an
|
|
1589
|
-
// unbuildable one with `point_action_invalid`, naming the op and the
|
|
1590
|
-
// action. A call naming no action falls through to the guards exactly
|
|
1591
|
-
// as before.
|
|
1592
1114
|
const actionBranch = (cmd: string, call: string) => ` const action$ = actionOf(msg)
|
|
1593
1115
|
if (null != action$) {
|
|
1594
1116
|
const op$ = actionop(action$, '${e.name}', '${cmd}')
|
|
@@ -1630,15 +1152,6 @@ ${actionBranch('load',
|
|
|
1630
1152
|
const hasCreate = e.ops.includes('create')
|
|
1631
1153
|
const hasUpdate = e.ops.includes('update')
|
|
1632
1154
|
|
|
1633
|
-
// Dispatch on the SENECA key. The record arriving here is a Seneca
|
|
1634
|
-
// entity's data, so its id lives at `id` whatever the API calls it —
|
|
1635
|
-
// dispatching on the API's key sent every save to `create`, leaving
|
|
1636
|
-
// update unreachable.
|
|
1637
|
-
// A MISADDRESSED UPDATE IS REFUSED, BUT ONLY ON THE UPDATE LEG. A
|
|
1638
|
-
// create needs no record key — the API assigns one — so an entity
|
|
1639
|
-
// whose update route addresses a different resource can still be
|
|
1640
|
-
// created. Dispatch is on `data.id`, so the refusal goes exactly
|
|
1641
|
-
// where the update would have.
|
|
1642
1155
|
const refuseUpdate = true === (e.idmisaddressed || {}).update ?
|
|
1643
1156
|
refuse('update') : ''
|
|
1644
1157
|
|
|
@@ -1654,25 +1167,6 @@ ${actionBranch('load',
|
|
|
1654
1167
|
? ` const res = await sdk.${e.acc}(${entArg}).create(data)`
|
|
1655
1168
|
: `${refuseUpdate} const res = await sdk.${e.acc}(${entArg}).update(data)`
|
|
1656
1169
|
|
|
1657
|
-
// ... and hand the API back its own key, which Seneca does not know
|
|
1658
|
-
// to send.
|
|
1659
|
-
//
|
|
1660
|
-
// A COMPOSITE KEY IS UNPACKED ONTO THE DATA. The write goes out as
|
|
1661
|
-
// a body plus path parameters, and the SDK reads those parameters
|
|
1662
|
-
// off the same object — so the parts have to be present under
|
|
1663
|
-
// their own names, not fused into `id`. On a create there is no id
|
|
1664
|
-
// yet and the caller supplies the parts directly, which is why this
|
|
1665
|
-
// only runs when an id is there.
|
|
1666
|
-
// THE COMPOSITE SPLIT CANNOT LIVE HERE, before the action branch,
|
|
1667
|
-
// even though that is where the single-key alias sits. The alias
|
|
1668
|
-
// only ever assigns; the split THROWS on an id that is not all its
|
|
1669
|
-
// parts, and an `action$` call is entitled to an id shaped however
|
|
1670
|
-
// that action's own route wants. Running it first turned
|
|
1671
|
-
// `action$: 'no_such_action'` into an id complaint, hiding the
|
|
1672
|
-
// error the caller needed. It is emitted after the action branch
|
|
1673
|
-
// instead — see compositeSave below, spliced where the parent
|
|
1674
|
-
// guards go, which is exactly the position the component already
|
|
1675
|
-
// documents as "after the action branch".
|
|
1676
1170
|
const compositeSave = 0 < eparts.length ? `
|
|
1677
1171
|
// Seneca carries this ${e.name}'s key as one \`id\`; the API addresses
|
|
1678
1172
|
// the record by ${eparts.map((p: string) => '`' + p + '`').join(' and ')}.
|
|
@@ -1746,20 +1240,6 @@ ${actionBranch('save',
|
|
|
1746
1240
|
}
|
|
1747
1241
|
|
|
1748
1242
|
if (e.cmds.includes('remove')) {
|
|
1749
|
-
// A REMOVE ACTION ANSWERS FOR ITSELF. The canonical remove has
|
|
1750
|
-
// nothing to hand back — the record is gone — so its handler
|
|
1751
|
-
// returns null and takes no `entize`. An action folded into
|
|
1752
|
-
// `remove` is a different endpoint with a response of its own
|
|
1753
|
-
// (`/meeting/{id}/archive` answers with the archive record), and
|
|
1754
|
-
// the other three cmds all pass an action's response through.
|
|
1755
|
-
// Applying the canonical "return null" to it threw that away, so
|
|
1756
|
-
// the action appeared to succeed and yielded nothing.
|
|
1757
|
-
//
|
|
1758
|
-
// `entize` is therefore named, never `_entize`. Naming it
|
|
1759
|
-
// conditionally on the entity HAVING a remove action does not work
|
|
1760
|
-
// and the type-check says so: the action branch is emitted for
|
|
1761
|
-
// every entity — that is what refuses an unknown `action$` on one
|
|
1762
|
-
// with no actions — so the parameter is always referenced.
|
|
1763
1243
|
Content(`
|
|
1764
1244
|
${jsProp('entity', e.name)}.cmd.remove.action =
|
|
1765
1245
|
async function remove_${e.name}(this: any, entize: any, msg: any) {
|
|
@@ -1883,7 +1363,6 @@ if ('undefined' !== typeof module) {
|
|
|
1883
1363
|
})
|
|
1884
1364
|
|
|
1885
1365
|
|
|
1886
|
-
// --- src/<Name>Provider-doc.ts ----------------------------------------------
|
|
1887
1366
|
|
|
1888
1367
|
const ProviderDoc = cmp(function ProviderDoc(props: any) {
|
|
1889
1368
|
const { provider } = props
|