@voxgig/sdkgen-infrapack 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sdk/model/target/seneca-provider.aon +6 -126
- package/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +256 -655
- package/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +0 -21
- package/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +190 -621
- package/package.json +7 -7
- package/sdkgen-package.json +2 -2
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
opRequestShape, opParams, ownPoint, entityPath,
|
|
7
7
|
collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
|
|
8
8
|
authorInfo, contributorList, isAuthActive, isHttpBasicAuth, jsKey, jsProp,
|
|
9
|
+
resolveAuthIn, resolveAuthName, resolveAuthPrefix,
|
|
9
10
|
SdkGenError,
|
|
10
11
|
PUBLISHER, PUBLISHER_URL,
|
|
11
12
|
pointSegments,
|
|
@@ -21,36 +22,8 @@ import {
|
|
|
21
22
|
import { Gitignore } from './Gitignore_seneca-provider'
|
|
22
23
|
|
|
23
24
|
|
|
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.
|
|
25
|
+
|
|
26
|
+
|
|
54
27
|
const CMD_OPS: Record<string, string[]> = {
|
|
55
28
|
list: ['list'],
|
|
56
29
|
load: ['load'],
|
|
@@ -59,28 +32,6 @@ const CMD_OPS: Record<string, string[]> = {
|
|
|
59
32
|
}
|
|
60
33
|
|
|
61
34
|
|
|
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
35
|
function cmdActions(ent: any, cmd: string): Record<string, string> {
|
|
85
36
|
const ops = CMD_OPS[cmd] || []
|
|
86
37
|
|
|
@@ -107,21 +58,13 @@ function cmdActions(ent: any, cmd: string): Record<string, string> {
|
|
|
107
58
|
}
|
|
108
59
|
|
|
109
60
|
|
|
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
61
|
function canonicalOps(ent: any): string[] {
|
|
119
62
|
return entityOps(ent).filter((opname: string) => {
|
|
120
63
|
const op = (ent.op || {})[opname]
|
|
121
64
|
const points: any[] = (op && op.points) || []
|
|
122
65
|
|
|
123
66
|
return points.some((pt: any) =>
|
|
124
|
-
null == (pt && pt.
|
|
67
|
+
null == (pt && pt.q && pt.q['$action']))
|
|
125
68
|
})
|
|
126
69
|
}
|
|
127
70
|
|
|
@@ -146,9 +89,6 @@ function entityActionList(ent: any):
|
|
|
146
89
|
}
|
|
147
90
|
|
|
148
91
|
|
|
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
92
|
function requiredKeys(ent: any, opname: string): string[] {
|
|
153
93
|
const idf = entityIdField(ent)
|
|
154
94
|
return opRequestShape(ent, opname).items
|
|
@@ -167,13 +107,6 @@ function addressKeys(ent: any, opname: string): string[] {
|
|
|
167
107
|
const parts = idParts(ent)
|
|
168
108
|
|
|
169
109
|
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
110
|
const shape = opRequestShape(ent, opname).items
|
|
178
111
|
for (const part of parts) {
|
|
179
112
|
if (!keys.includes(part) &&
|
|
@@ -194,33 +127,6 @@ function addressKeys(ent: any, opname: string): string[] {
|
|
|
194
127
|
}
|
|
195
128
|
|
|
196
129
|
|
|
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
130
|
function idParts(ent: any): string[] {
|
|
225
131
|
const parts = ent?.id?.parts
|
|
226
132
|
return Array.isArray(parts) && 1 < parts.length ?
|
|
@@ -234,18 +140,6 @@ function idSep(ent: any): string {
|
|
|
234
140
|
}
|
|
235
141
|
|
|
236
142
|
|
|
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
143
|
function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string, string> } | null {
|
|
250
144
|
const parts = idParts(ent)
|
|
251
145
|
if (0 < parts.length) {
|
|
@@ -262,18 +156,27 @@ function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string,
|
|
|
262
156
|
return null
|
|
263
157
|
}
|
|
264
158
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return {
|
|
159
|
+
const from = ent?.id?.from
|
|
160
|
+
const path = null != from && 'object' === typeof from ? from[rk] : null
|
|
161
|
+
return {
|
|
162
|
+
parts: [rk],
|
|
163
|
+
sep: idSep(ent),
|
|
164
|
+
...(null != path && '' !== String(path) ? { from: { [rk]: String(path) } } : {}),
|
|
165
|
+
}
|
|
268
166
|
}
|
|
269
167
|
|
|
270
168
|
|
|
169
|
+
// The model's identifier when the load match carries it, else the terminal
|
|
170
|
+
// path parameter, else a lone required query parameter: the rule the SDK's
|
|
171
|
+
// offline transport keys its store by.
|
|
271
172
|
function recordKey(ent: any): string {
|
|
272
173
|
const idf = entityIdField(ent)
|
|
273
174
|
if (null != idf && '' !== idf) {
|
|
274
175
|
return String(idf)
|
|
275
176
|
}
|
|
276
177
|
|
|
178
|
+
let fallback = ''
|
|
179
|
+
|
|
277
180
|
for (const opname of ['load', 'remove', 'update']) {
|
|
278
181
|
const op = (ent.op || {})[opname]
|
|
279
182
|
if (null == op || 0 === (op.points || []).length) {
|
|
@@ -281,50 +184,66 @@ function recordKey(ent: any): string {
|
|
|
281
184
|
}
|
|
282
185
|
|
|
283
186
|
const canonical = op.points.filter((pt: any) =>
|
|
284
|
-
null == (pt && pt.
|
|
187
|
+
null == (pt && pt.q && pt.q['$action']))
|
|
285
188
|
const point = ownPoint(0 < canonical.length ? canonical : op.points)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (0 < vars.length) {
|
|
293
|
-
return String(vars[vars.length - 1].var)
|
|
189
|
+
const segs = pointSegments(point)
|
|
190
|
+
const last = segs[segs.length - 1]
|
|
191
|
+
|
|
192
|
+
if (null != last && null != last.var) {
|
|
193
|
+
return String(last.var)
|
|
294
194
|
}
|
|
295
195
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const query = (point && point.args && point.args.query) || []
|
|
299
|
-
const reqdQuery = query.filter((q: any) => false !== q.reqd)
|
|
196
|
+
const query = (point && point.g && point.g.query) || []
|
|
197
|
+
const reqdQuery = query.filter((q: any) => false !== q.r)
|
|
300
198
|
if (1 === reqdQuery.length) {
|
|
301
|
-
return String(reqdQuery[0].
|
|
199
|
+
return String(reqdQuery[0].n)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A literal-terminal route names a facet of a record: a last resort.
|
|
203
|
+
if ('' === fallback) {
|
|
204
|
+
const vars = segs.filter((seg: any) => null != seg.var)
|
|
205
|
+
if (0 < vars.length) {
|
|
206
|
+
fallback = String(vars[vars.length - 1].var)
|
|
207
|
+
}
|
|
302
208
|
}
|
|
303
209
|
}
|
|
304
210
|
|
|
305
|
-
return 'id'
|
|
211
|
+
return '' !== fallback ? fallback : 'id'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// Does a create have to SEND the record key, or does the API assign it?
|
|
216
|
+
function rkOnCreate(ent: any): boolean {
|
|
217
|
+
const rk = recordKey(ent)
|
|
218
|
+
|
|
219
|
+
if ('id' === rk || 0 < idParts(ent).length || null == (ent.op || {}).create) {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return opRequestShape(ent, 'create').items
|
|
224
|
+
.some((it: any) => it.name === rk && !it.optional)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
// Is `<provider>_id` free for this entity, or a name it already uses itself?
|
|
229
|
+
function parkFree(ent: any, parked: string): boolean {
|
|
230
|
+
const taken = new Set<string>([
|
|
231
|
+
recordKey(ent),
|
|
232
|
+
...idParts(ent),
|
|
233
|
+
...parentKeys(ent),
|
|
234
|
+
...Object.values(ent.fields || {}).map((f: any) => String(f.n)),
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
return !taken.has(parked)
|
|
306
238
|
}
|
|
307
239
|
|
|
308
240
|
|
|
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
241
|
function guardName(e: any, key: string): string {
|
|
315
242
|
return `need_${e.name}_${String(key).replace(/[^A-Za-z0-9_$]/g, '_')}`
|
|
316
243
|
.replace(/^need_(\d)/, 'need__$1')
|
|
317
244
|
}
|
|
318
245
|
|
|
319
246
|
|
|
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
247
|
function opParentKeys(ent: any, opname: string): string[] {
|
|
329
248
|
const rk = recordKey(ent)
|
|
330
249
|
const op = (ent.op || {})[opname]
|
|
@@ -336,8 +255,8 @@ function opParentKeys(ent: any, opname: string): string[] {
|
|
|
336
255
|
const seen = new Set<string>()
|
|
337
256
|
|
|
338
257
|
for (const p of opParams(op)) {
|
|
339
|
-
const name = String((p as any).
|
|
340
|
-
if (false !== (p as any).
|
|
258
|
+
const name = String((p as any).n)
|
|
259
|
+
if (false !== (p as any).r && name !== rk && name !== 'id') {
|
|
341
260
|
seen.add(name)
|
|
342
261
|
}
|
|
343
262
|
}
|
|
@@ -346,13 +265,6 @@ function opParentKeys(ent: any, opname: string): string[] {
|
|
|
346
265
|
}
|
|
347
266
|
|
|
348
267
|
|
|
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
268
|
function parentKeys(ent: any): string[] {
|
|
357
269
|
const seen = new Set<string>()
|
|
358
270
|
|
|
@@ -366,20 +278,6 @@ function parentKeys(ent: any): string[] {
|
|
|
366
278
|
}
|
|
367
279
|
|
|
368
280
|
|
|
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
281
|
function fieldKind(type: any): string {
|
|
384
282
|
if (Array.isArray(type)) {
|
|
385
283
|
return 'string'
|
|
@@ -396,21 +294,12 @@ function fieldKind(type: any): string {
|
|
|
396
294
|
}
|
|
397
295
|
|
|
398
296
|
|
|
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
297
|
function parentEntityOf(key: string, names: string[]): string {
|
|
403
298
|
const stem = key.replace(/_id$/, '')
|
|
404
299
|
return names.includes(stem) ? stem : ''
|
|
405
300
|
}
|
|
406
301
|
|
|
407
302
|
|
|
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
303
|
function providerRepo(model: any, lower: string, tname: string):
|
|
415
304
|
{ url: string, path: string } {
|
|
416
305
|
const host = model?.main?.[KIT]?.repo?.host || 'github.com'
|
|
@@ -437,9 +326,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
437
326
|
const { target, ctx$ } = props
|
|
438
327
|
const { model } = ctx$
|
|
439
328
|
|
|
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
329
|
const targets = model.main[KIT].target || {}
|
|
444
330
|
if (null == targets.ts) {
|
|
445
331
|
throw new SdkGenError(
|
|
@@ -449,12 +335,12 @@ const Main = cmp(function Main(props: any) {
|
|
|
449
335
|
'then regenerate.')
|
|
450
336
|
}
|
|
451
337
|
|
|
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`
|
|
338
|
+
const Name = model.const.Name
|
|
339
|
+
const lower = String(model.const.name)
|
|
340
|
+
const ENV = envName(model)
|
|
341
|
+
const sdkClass = `${Name}SDK`
|
|
342
|
+
const pluginName = `${Name}Provider`
|
|
343
|
+
const fileBase = `${lower}-provider`
|
|
458
344
|
|
|
459
345
|
// The SDK is a PUBLISHED dependency, not a path: this package lives in its
|
|
460
346
|
// own repo. Its name is whatever the ts target publishes under, pin
|
|
@@ -464,45 +350,23 @@ const Main = cmp(function Main(props: any) {
|
|
|
464
350
|
const sdkPkg = packageName(model, 'npm')
|
|
465
351
|
const sdkVersion = packageVersion(model, 'ts')
|
|
466
352
|
|
|
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
353
|
const entityColl = entityCollection(model)
|
|
479
354
|
|
|
480
355
|
const activeEntities = Object.keys(entityColl).sort()
|
|
481
356
|
.map((key: string) => entityColl[key])
|
|
482
357
|
.filter((ent: any) => false !== ent.active)
|
|
483
358
|
|
|
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
359
|
const entityNames = activeEntities.map((ent: any) => ent.name)
|
|
488
360
|
|
|
489
361
|
const entities = activeEntities
|
|
490
362
|
.map((ent: any) => {
|
|
491
363
|
const parents = parentKeys(ent)
|
|
492
364
|
|
|
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
365
|
const parentOf: Record<string, string> = {}
|
|
497
366
|
for (const key of parents) {
|
|
498
367
|
parentOf[key] = parentEntityOf(key, entityNames)
|
|
499
368
|
}
|
|
500
369
|
|
|
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
370
|
const opParents: Record<string, string[]> = {}
|
|
507
371
|
for (const opname of entityOps(ent)) {
|
|
508
372
|
opParents[opname] = opParentKeys(ent, opname)
|
|
@@ -511,28 +375,14 @@ const Main = cmp(function Main(props: any) {
|
|
|
511
375
|
return {
|
|
512
376
|
ent,
|
|
513
377
|
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
378
|
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
379
|
path: entityPath(ent),
|
|
526
380
|
cls: entityClassName(ent, entityColl),
|
|
527
381
|
ops: entityOps(ent),
|
|
528
382
|
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
383
|
rk: recordKey(ent),
|
|
384
|
+
rkoncreate: rkOnCreate(ent),
|
|
385
|
+
parkfree: parkFree(ent, lower + '_id'),
|
|
536
386
|
// The composite key, when this API addresses a record by several
|
|
537
387
|
// path params at once. The doc and test emitters in Extras need the
|
|
538
388
|
// same answer the handler emitters use: a test that addresses a
|
|
@@ -540,41 +390,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
540
390
|
// rejects.
|
|
541
391
|
idparts: idParts(ent),
|
|
542
392
|
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
393
|
idmisaddressed: ['remove', 'update'].reduce(
|
|
579
394
|
(acc: Record<string, boolean>, opname: string) => {
|
|
580
395
|
acc[opname] = null != (ent.op || {})[opname] &&
|
|
@@ -594,8 +409,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
594
409
|
keys.includes(recordKey(ent))
|
|
595
410
|
return acc
|
|
596
411
|
}, {}),
|
|
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
412
|
idfrom: (ent?.id?.from) || {},
|
|
600
413
|
parents,
|
|
601
414
|
parentOf,
|
|
@@ -613,30 +426,15 @@ const Main = cmp(function Main(props: any) {
|
|
|
613
426
|
acc[cmd] = cmdActions(ent, cmd)
|
|
614
427
|
return acc
|
|
615
428
|
}, {}),
|
|
616
|
-
// The same information flattened, for the README and the generated
|
|
617
|
-
// tests: [{ cmd, op, action, path }].
|
|
618
429
|
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
430
|
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
431
|
fields: (() => {
|
|
634
|
-
const req = (ent.fields ||
|
|
635
|
-
.filter((f: any) => false !== f.
|
|
432
|
+
const req = (Object.values(ent.fields || {}))
|
|
433
|
+
.filter((f: any) => false !== f.r)
|
|
636
434
|
.map((f: any) => ({
|
|
637
|
-
name: f.
|
|
638
|
-
kind: fieldKind(f.
|
|
639
|
-
parentEntity: parentEntityOf(f.
|
|
435
|
+
name: f.n,
|
|
436
|
+
kind: fieldKind(f.t),
|
|
437
|
+
parentEntity: parentEntityOf(f.n, entityNames),
|
|
640
438
|
}))
|
|
641
439
|
|
|
642
440
|
const have = new Set(req.map((f: any) => f.name))
|
|
@@ -677,27 +475,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
677
475
|
// once by the external pass — see cmp/ExternalTarget.
|
|
678
476
|
const sdkrel = ctx$.sdkrelpath || '..'
|
|
679
477
|
|
|
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
478
|
const sdkRepoUrl = String(repoInfo(model).repoUrl || '')
|
|
702
479
|
const sdkTag = 'v' + sdkVersion
|
|
703
480
|
const sdkRepoDir = sdkRepoUrl.replace(/[/]+$/, '').split('/').pop() || 'sdk'
|
|
@@ -708,22 +485,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
708
485
|
const sdkPinned = '' !== sdkRepoUrl
|
|
709
486
|
const sdkSrc = sdkPinned ? SDK_SRC_DIR + '/' + sdkRepoDir : sdkrel
|
|
710
487
|
|
|
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
488
|
const live = model?.main?.[KIT]?.test?.live || {}
|
|
728
489
|
const servers = (model?.main?.[KIT]?.info?.servers || [])
|
|
729
490
|
const specBase = 0 < servers.length ? String(servers[0].url || '') : ''
|
|
@@ -748,9 +509,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
748
509
|
const provider = {
|
|
749
510
|
Name, lower, ENV, sdkClass, pluginName, fileBase,
|
|
750
511
|
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
512
|
sdkDep,
|
|
755
513
|
// Whether that dependency comes from outside a registry. The generated
|
|
756
514
|
// CI note says so, because "npm install is all you need" stops being
|
|
@@ -761,61 +519,34 @@ const Main = cmp(function Main(props: any) {
|
|
|
761
519
|
// that says the wrong one is worse than no note.
|
|
762
520
|
sdkDepKind: sdkDep.startsWith('github:') ? 'git' :
|
|
763
521
|
(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
522
|
sdkInstallFlag: sdkDep.startsWith('github:') ? ' --allow-git=all' :
|
|
778
523
|
(sdkDep.startsWith('http') ? ' --allow-remote=all' : ''),
|
|
779
524
|
repoUrl: repo.url,
|
|
780
|
-
// The SDK's own repo, for pointing at the companion test server which is
|
|
781
|
-
// only distributed in source.
|
|
782
525
|
sdkRepoUrl,
|
|
783
526
|
sdkTag,
|
|
784
527
|
sdkRepoDir,
|
|
785
528
|
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
529
|
sdkSrc,
|
|
789
530
|
api: apiName(model),
|
|
790
531
|
version: packageVersion(model, target.name),
|
|
791
532
|
liveBase,
|
|
792
533
|
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
534
|
publisher: PUBLISHER,
|
|
797
535
|
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
536
|
probePath: (entities.find((e: any) =>
|
|
802
537
|
'' !== 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
538
|
pkgName: providerPackage(model, lower, target.name),
|
|
807
539
|
// Whether this API authenticates at all. Decides if the plugin plumbs a
|
|
808
540
|
// credential: the SDK's auth stage emits nothing for an auth-inactive
|
|
809
541
|
// model and strips the authorization header regardless of options, so
|
|
810
542
|
// plumbing one anyway produces a credential path that cannot work.
|
|
811
543
|
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
544
|
authBasic: isAuthActive(model) && isHttpBasicAuth(model),
|
|
545
|
+
// Where the credential goes, resolved as the SDK's auth stage does.
|
|
546
|
+
authIn: resolveAuthIn(model),
|
|
547
|
+
authName: 'header' === resolveAuthIn(model) ?
|
|
548
|
+
resolveAuthName(model).toLowerCase() : resolveAuthName(model),
|
|
549
|
+
authPrefix: resolveAuthPrefix(model),
|
|
819
550
|
}
|
|
820
551
|
|
|
821
552
|
// `.gitignore` is EMITTED rather than copied — npm strips that filename
|
|
@@ -824,8 +555,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
824
555
|
// target calls its own.
|
|
825
556
|
Gitignore({})
|
|
826
557
|
|
|
827
|
-
// Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, tsfmt.json and
|
|
828
|
-
// both tsconfigs. Same for every provider.
|
|
829
558
|
Copy({
|
|
830
559
|
from: 'tm/' + target.name,
|
|
831
560
|
replace: { ...ctx$.stdrep },
|
|
@@ -843,36 +572,6 @@ const Main = cmp(function Main(props: any) {
|
|
|
843
572
|
})
|
|
844
573
|
|
|
845
574
|
|
|
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
575
|
function sdkDependency(model: any, target: any, provider: any): string {
|
|
877
576
|
const dep = model?.main?.[KIT]?.target?.[target.name]?.sdk?.dep || {}
|
|
878
577
|
|
|
@@ -913,8 +612,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
|
|
|
913
612
|
}
|
|
914
613
|
|
|
915
614
|
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
615
|
const asset = String(dep.asset || '').trim() || (
|
|
919
616
|
provider.sdkPkg.replace(/^@/, '').replace(/\//g, '-') +
|
|
920
617
|
'-' + provider.sdkVersion + '.tgz')
|
|
@@ -926,7 +623,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
|
|
|
926
623
|
}
|
|
927
624
|
|
|
928
625
|
|
|
929
|
-
// --- package.json -----------------------------------------------------------
|
|
930
626
|
|
|
931
627
|
const PackageJson = cmp(function PackageJson(props: any) {
|
|
932
628
|
const { provider, target } = props
|
|
@@ -934,19 +630,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
934
630
|
|
|
935
631
|
const deps = collectDeps(model, target.name, target.deps, props.ctx$.log)
|
|
936
632
|
|
|
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
633
|
const dep = (kind: string) => {
|
|
951
634
|
const out: Record<string, string> = {}
|
|
952
635
|
const kinds = (d: any) => String((d.raw && d.raw.kind) || '')
|
|
@@ -978,9 +661,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
978
661
|
keywords: ['seneca', provider.lower, `${provider.lower}-provider`,
|
|
979
662
|
provider.publisher.toLowerCase(), 'sdk'],
|
|
980
663
|
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
664
|
...(0 < contributors.length ? { contributors } : {}),
|
|
985
665
|
license: 'MIT',
|
|
986
666
|
repository: { type: 'git', url: `git+${provider.repoUrl}.git` },
|
|
@@ -1012,17 +692,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
1012
692
|
'npm run build && npm run test && npm run repo-tag && ' +
|
|
1013
693
|
'npm publish --access public --registry https://registry.npmjs.org',
|
|
1014
694
|
},
|
|
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
695
|
files: ['dist', 'doc', 'src/**/*.ts', 'LICENSE'],
|
|
1027
696
|
engines: { node: '>=24' },
|
|
1028
697
|
dependencies: {
|
|
@@ -1042,11 +711,12 @@ const PackageJson = cmp(function PackageJson(props: any) {
|
|
|
1042
711
|
})
|
|
1043
712
|
|
|
1044
713
|
|
|
1045
|
-
// --- src/<name>-provider.ts -------------------------------------------------
|
|
1046
714
|
|
|
1047
715
|
const ProviderSource = cmp(function ProviderSource(props: any) {
|
|
1048
716
|
const { provider } = props
|
|
1049
717
|
|
|
718
|
+
const translated = provider.entities.filter((e: any) => null != idSpec(e.ent))
|
|
719
|
+
|
|
1050
720
|
Folder({ name: 'src' }, () => {
|
|
1051
721
|
File({ name: `${provider.fileBase}.ts` }, () => {
|
|
1052
722
|
Content(`/* Generated by @voxgig/sdkgen. Do not edit. */
|
|
@@ -1145,8 +815,8 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1145
815
|
|
|
1146
816
|
|
|
1147
817
|
// The SDK argument for an ACTION on a read cmd: everything the caller sent
|
|
1148
|
-
// minus Seneca's own directives, with the
|
|
1149
|
-
// the \`$action\` selector the SDK dispatches on.
|
|
818
|
+
// minus Seneca's own directives, with the id translated into the API's own
|
|
819
|
+
// keys, plus the \`$action\` selector the SDK dispatches on.
|
|
1150
820
|
//
|
|
1151
821
|
// WIDER than the canonical argument on purpose. An action route has its own
|
|
1152
822
|
// parameters — GitHub's merge takes commit_title and merge_method, which the
|
|
@@ -1154,14 +824,21 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1154
824
|
// required keys would strip the action's whole payload. The SDK still
|
|
1155
825
|
// validates: an action whose own point cannot be built is refused with
|
|
1156
826
|
// \`point_action_invalid\` rather than sent somewhere else.
|
|
1157
|
-
function actionq(q: any,
|
|
827
|
+
function actionq(q: any, ${0 === translated.length ? '' : 'name: string, '}action: string) {
|
|
1158
828
|
const out = cleanq(q)
|
|
829
|
+
${0 === translated.length ? '' : `
|
|
830
|
+
// An action addresses the SAME record the canonical route does, so the id
|
|
831
|
+
// is translated the same way a write translates it: split into the path
|
|
832
|
+
// parameters the API names, under the names it names them by. Sending the
|
|
833
|
+
// joined id as one terminal parameter asks for \`owner0/repo0\` as a repo.
|
|
834
|
+
const own = Object.prototype.hasOwnProperty
|
|
835
|
+
const spec = own.call(ID_SPEC, name) ? ID_SPEC[name] : null
|
|
1159
836
|
|
|
1160
|
-
if (
|
|
1161
|
-
out
|
|
837
|
+
if (null != spec && null != out.id) {
|
|
838
|
+
Object.assign(out, splitid(name, out.id, 'action'))
|
|
1162
839
|
delete out.id
|
|
1163
840
|
}
|
|
1164
|
-
|
|
841
|
+
`}
|
|
1165
842
|
out.$action = action
|
|
1166
843
|
return out
|
|
1167
844
|
}
|
|
@@ -1203,6 +880,15 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1203
880
|
'${provider.pkgName}: ${e.name} ' + cmd + ': ${k} is required'
|
|
1204
881
|
)
|
|
1205
882
|
}
|
|
883
|
+
|
|
884
|
+
// A path parameter is one value. A loaded record can carry an object
|
|
885
|
+
// under the same name, and that must not be sent as a URL segment.
|
|
886
|
+
if ('object' === typeof value) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
'${provider.pkgName}: ${e.name} ' + cmd + ': ${k} must be a single ' +
|
|
889
|
+
'value to build the request path, not an object'
|
|
890
|
+
)
|
|
891
|
+
}
|
|
1206
892
|
return value
|
|
1207
893
|
}
|
|
1208
894
|
|
|
@@ -1212,36 +898,15 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1212
898
|
})
|
|
1213
899
|
}
|
|
1214
900
|
|
|
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
|
-
const translated = provider.entities.filter((e: any) => null != idSpec(e.ent))
|
|
1236
|
-
|
|
1237
901
|
if (0 < translated.length) {
|
|
1238
902
|
const rows = translated.map((e: any) => {
|
|
1239
903
|
const spec: any = idSpec(e.ent)
|
|
1240
904
|
const from = null == spec.from ? '' :
|
|
1241
905
|
`, from: { ${Object.keys(spec.from).sort()
|
|
1242
906
|
.map((k: string) => `${jsKey(k)}: '${spec.from[k]}'`).join(', ')} }`
|
|
907
|
+
const park = false === e.parkfree ? ', park: false' : ''
|
|
1243
908
|
return ` ${jsKey(e.name)}: { parts: [${
|
|
1244
|
-
spec.parts.map((p: string) => `'${p}'`).join(', ')}], sep: '${spec.sep}'${from} },`
|
|
909
|
+
spec.parts.map((p: string) => `'${p}'`).join(', ')}], sep: '${spec.sep}'${from}${park} },`
|
|
1245
910
|
}).join('\n')
|
|
1246
911
|
|
|
1247
912
|
Content(` // HOW EACH ENTITY'S id MAPS TO THE API'S OWN KEYS, from the model.
|
|
@@ -1258,7 +923,9 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
|
|
|
1258
923
|
// field's name: github returns a repo's owner as an OBJECT
|
|
1259
924
|
// (\`owner.login\`) and its name as \`name\`, never \`repo\`.
|
|
1260
925
|
// A part missing here cannot be read back off a response.
|
|
1261
|
-
|
|
926
|
+
// \`park\` false where \`${provider.lower}_id\` is a name this entity
|
|
927
|
+
// already uses, so the API's own id is left where it is.
|
|
928
|
+
const ID_SPEC: Record<string, { parts: string[], sep: string, from?: Record<string, string>, park?: boolean }> = {
|
|
1262
929
|
${rows}
|
|
1263
930
|
}
|
|
1264
931
|
|
|
@@ -1309,7 +976,7 @@ ${rows}
|
|
|
1309
976
|
// THE ADDRESSING KEY WINS over an \`id\` the response already carries. A
|
|
1310
977
|
// response often has both — github's pull has a global database \`id\` and
|
|
1311
978
|
// a repo-scoped \`number\` — and the unrelated one is no use for addressing
|
|
1312
|
-
// anything. It is kept as \`${provider.lower}_id
|
|
979
|
+
// anything. It is kept as \`${provider.lower}_id\`, unless \`park\` forbids.
|
|
1313
980
|
function joinid(name: string, data: any, vals?: any) {
|
|
1314
981
|
const spec = ID_SPEC[name]
|
|
1315
982
|
if (null == data) {
|
|
@@ -1335,7 +1002,7 @@ ${rows}
|
|
|
1335
1002
|
}
|
|
1336
1003
|
|
|
1337
1004
|
if (null != id) {
|
|
1338
|
-
if (null != data.id && String(data.id) !== id &&
|
|
1005
|
+
if (false !== spec.park && null != data.id && String(data.id) !== id &&
|
|
1339
1006
|
null == ${jsProp('data', provider.lower + '_id')}) {
|
|
1340
1007
|
${jsProp('data', provider.lower + '_id')} = data.id
|
|
1341
1008
|
}
|
|
@@ -1349,14 +1016,6 @@ ${rows}
|
|
|
1349
1016
|
`)
|
|
1350
1017
|
}
|
|
1351
1018
|
|
|
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
1019
|
Content(` // The custom actions each cmd can reach, as action -> SDK op. An action
|
|
1361
1020
|
// is an alternative POINT of an ordinary op (\`select.$action\` in the API
|
|
1362
1021
|
// model), so \`save$\` routes by this map rather than assuming update: an
|
|
@@ -1370,12 +1029,6 @@ ${rows}
|
|
|
1370
1029
|
const name = String(cmd.val$ ?? cmd)
|
|
1371
1030
|
const map = e.actions[name] || {}
|
|
1372
1031
|
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
1032
|
Content(` ${name}: {${names.map((a: string) =>
|
|
1380
1033
|
` [${JSON.stringify(a)}]: '${map[a]}'`).join(',')}${0 < names.length ? ' ' : ''}},
|
|
1381
1034
|
`)
|
|
@@ -1450,8 +1103,6 @@ ${rows}
|
|
|
1450
1103
|
|
|
1451
1104
|
`)
|
|
1452
1105
|
|
|
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
1106
|
Content(` const entity: any = {
|
|
1456
1107
|
`)
|
|
1457
1108
|
each(provider.entities, (e: any) => {
|
|
@@ -1472,42 +1123,20 @@ ${rows}
|
|
|
1472
1123
|
`)
|
|
1473
1124
|
|
|
1474
1125
|
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
1126
|
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
1127
|
const espec = idSpec(e.ent)
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
(e.opParents[cmd] || [])
|
|
1128
|
+
// Guards of ONE op: save's create and update branches guard their own.
|
|
1129
|
+
const guard = (opname: string, src: string, label = opname, ind = ' ') => {
|
|
1130
|
+
const keys = e.opParents[opname] || []
|
|
1496
1131
|
|
|
1497
1132
|
return keys
|
|
1498
1133
|
.filter((k: string) => !eparts.includes(k))
|
|
1499
1134
|
.map((k: string) =>
|
|
1500
|
-
|
|
1135
|
+
`${ind}${guardName(e, k)}(${jsProp(src, k)}, '${label}')\n`)
|
|
1501
1136
|
.join('')
|
|
1502
1137
|
}
|
|
1503
1138
|
|
|
1504
|
-
|
|
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
|
-
const refuse = (cmd: string) => {
|
|
1139
|
+
const refuse = (cmd: string, ind = ' ') => {
|
|
1511
1140
|
if (true !== (e.idmisaddressed || {})[cmd]) {
|
|
1512
1141
|
return ''
|
|
1513
1142
|
}
|
|
@@ -1515,31 +1144,17 @@ ${rows}
|
|
|
1515
1144
|
recordKey(e.ent)
|
|
1516
1145
|
const addresses = addressKeys(e.ent, cmd)
|
|
1517
1146
|
|
|
1518
|
-
return
|
|
1147
|
+
return `${ind}misaddressed('${e.name}', '${cmd}', '${key}', ` +
|
|
1519
1148
|
`[${addresses.map((k: string) => `'${k}'`).join(', ')}])
|
|
1520
1149
|
`
|
|
1521
1150
|
}
|
|
1522
1151
|
|
|
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
1152
|
const rk = recordKey(e.ent)
|
|
1526
1153
|
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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.
|
|
1154
|
+
// Named only where there is a spec to translate the id through.
|
|
1155
|
+
const aq = `actionq(msg.q, ${0 === translated.length ? '' :
|
|
1156
|
+
`'${e.name}', `}action$)`
|
|
1157
|
+
|
|
1543
1158
|
const sdkArg = (opname: string) => {
|
|
1544
1159
|
const keys = addressKeys(e.ent, opname)
|
|
1545
1160
|
if (0 === keys.length) {
|
|
@@ -1554,41 +1169,24 @@ ${rows}
|
|
|
1554
1169
|
`${jsKey(k)}: ${jsProp('q', k === rk ? 'id' : k)}`).join(', ')} }`
|
|
1555
1170
|
}
|
|
1556
1171
|
|
|
1557
|
-
//
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
//
|
|
1561
|
-
const entArg =
|
|
1562
|
-
`null == key ? undefined : { match: key }`
|
|
1172
|
+
// Seneca's `id` and the API's key differ: composite, or not called id.
|
|
1173
|
+
const keyed = null != espec
|
|
1174
|
+
|
|
1175
|
+
// The entity-options argument carrying a write's addressing values.
|
|
1176
|
+
const entArg = !keyed ? '' : `null == key ? undefined : { match: key }`
|
|
1563
1177
|
|
|
1564
1178
|
const splitLine = (cmd: string) => 0 === eparts.length ? '' :
|
|
1565
|
-
` const key = splitid('${e.name}',
|
|
1179
|
+
` const key = splitid('${e.name}', q.id, '${cmd}')\n`
|
|
1566
1180
|
|
|
1567
|
-
// The data hop, plus the id
|
|
1568
|
-
//
|
|
1569
|
-
// values through, because the response may not repeat them.
|
|
1181
|
+
// The data hop, plus the id from the values this request addressed the
|
|
1182
|
+
// record with, which the response need not repeat.
|
|
1570
1183
|
const out = (expr: string, vals?: string) =>
|
|
1571
|
-
|
|
1184
|
+
!keyed ? `plain(${expr})` :
|
|
1572
1185
|
`joinid('${e.name}', plain(${expr})${null == vals ? '' : ', ' + vals})`
|
|
1573
1186
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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.
|
|
1187
|
+
const loadVals = 0 < eparts.length ? 'key' :
|
|
1188
|
+
!keyed ? undefined : `{ ${jsKey(rk)}: q.id }`
|
|
1189
|
+
|
|
1592
1190
|
const actionBranch = (cmd: string, call: string) => ` const action$ = actionOf(msg)
|
|
1593
1191
|
if (null != action$) {
|
|
1594
1192
|
const op$ = actionop(action$, '${e.name}', '${cmd}')
|
|
@@ -1602,7 +1200,7 @@ ${call} }
|
|
|
1602
1200
|
async function list_${e.name}(this: any, entize: any, msg: any) {
|
|
1603
1201
|
const q = cleanq(msg.q)
|
|
1604
1202
|
${actionBranch('list',
|
|
1605
|
-
` const found = await this.shared.sdk.${e.acc}()[op$](
|
|
1203
|
+
` const found = await this.shared.sdk.${e.acc}()[op$](${aq})
|
|
1606
1204
|
return found.map((data: any) => entize(${out('data')}))
|
|
1607
1205
|
`)}${guard('list', 'q')} const list = await this.shared.sdk.${e.acc}().list(q)
|
|
1608
1206
|
return list.map((data: any) => entize(${out('data')}))
|
|
@@ -1617,10 +1215,10 @@ ${actionBranch('list',
|
|
|
1617
1215
|
async function load_${e.name}(this: any, entize: any, msg: any) {
|
|
1618
1216
|
const q = cleanq(msg.q)
|
|
1619
1217
|
${actionBranch('load',
|
|
1620
|
-
` const hit = await ornull(() => this.shared.sdk.${e.acc}()[op$](
|
|
1218
|
+
` const hit = await ornull(() => this.shared.sdk.${e.acc}()[op$](${aq}))
|
|
1621
1219
|
return null == hit ? null : entize(${out('hit')})
|
|
1622
1220
|
`)}${guard('load', 'q')}${splitLine('load')} const res = await ornull(() => this.shared.sdk.${e.acc}().load(${sdkArg('load')}))
|
|
1623
|
-
return null == res ? null : entize(${out('res',
|
|
1221
|
+
return null == res ? null : entize(${out('res', loadVals)})
|
|
1624
1222
|
}
|
|
1625
1223
|
|
|
1626
1224
|
`)
|
|
@@ -1630,50 +1228,19 @@ ${actionBranch('load',
|
|
|
1630
1228
|
const hasCreate = e.ops.includes('create')
|
|
1631
1229
|
const hasUpdate = e.ops.includes('update')
|
|
1632
1230
|
|
|
1633
|
-
|
|
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
|
-
const refuseUpdate = true === (e.idmisaddressed || {}).update ?
|
|
1643
|
-
refuse('update') : ''
|
|
1231
|
+
const refuseUpdate = true === (e.idmisaddressed || {}).update
|
|
1644
1232
|
|
|
1645
|
-
const
|
|
1646
|
-
? (('' === refuseUpdate) ? ` const res = null == data.id
|
|
1647
|
-
? await sdk.${e.acc}(${entArg}).create(data)
|
|
1648
|
-
: await sdk.${e.acc}(${entArg}).update(data)` : ` if (null != data.id) {
|
|
1649
|
-
${refuseUpdate.replace(/\n$/, '')}
|
|
1650
|
-
}
|
|
1233
|
+
const isUpdate = keyed ? 'null != key' : 'null != data.id'
|
|
1651
1234
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
const compositeSave = 0 < eparts.length ? `
|
|
1235
|
+
// Deleting a name the entity owns strips a value the caller supplied.
|
|
1236
|
+
const dropPark = false === e.parkfree ? '' : `
|
|
1237
|
+
// \`${provider.lower}_id\` is this provider's own bookkeeping — the
|
|
1238
|
+
// API's unrelated \`id\`, parked by joinid(). It is not a field of the
|
|
1239
|
+
// API's write schema, so it must not travel in the request body.
|
|
1240
|
+
delete ${jsProp('data', provider.lower + '_id')}
|
|
1241
|
+
`
|
|
1242
|
+
|
|
1243
|
+
const keyBlock = 0 < eparts.length ? `
|
|
1677
1244
|
// Seneca carries this ${e.name}'s key as one \`id\`; the API addresses
|
|
1678
1245
|
// the record by ${eparts.map((p: string) => '`' + p + '`').join(' and ')}.
|
|
1679
1246
|
//
|
|
@@ -1692,13 +1259,7 @@ ${actionBranch('load',
|
|
|
1692
1259
|
if (null != data.id) {
|
|
1693
1260
|
key = splitid('${e.name}', data.id, 'save')
|
|
1694
1261
|
}
|
|
1695
|
-
|
|
1696
|
-
// \`${provider.lower}_id\` is this provider's own bookkeeping — the
|
|
1697
|
-
// API's unrelated \`id\`, parked by joinid() so it is not lost. It is
|
|
1698
|
-
// not a field of the API's write schema, so it must not travel in the
|
|
1699
|
-
// request body.
|
|
1700
|
-
delete ${jsProp('data', provider.lower + '_id')}
|
|
1701
|
-
|
|
1262
|
+
${dropPark}
|
|
1702
1263
|
// AND NEITHER DOES THE JOINED \`id\`. It is Seneca's key for this
|
|
1703
1264
|
// record, not the API's: a composite ${e.name} is addressed by
|
|
1704
1265
|
// \`${eparts.join('\` and \`')}\`, which travel as path parameters in
|
|
@@ -1707,65 +1268,73 @@ ${actionBranch('load',
|
|
|
1707
1268
|
// which matches a request against a stored record, then looked for a
|
|
1708
1269
|
// record whose own \`id\` was that joined string and found none.
|
|
1709
1270
|
delete data.id
|
|
1710
|
-
` : ''
|
|
1711
|
-
|
|
1712
|
-
const alias = 0 < eparts.length ? '' : 'id' === rk ? '' :
|
|
1713
|
-
`
|
|
1271
|
+
` : !keyed ? '' : `
|
|
1714
1272
|
// This API keys a ${e.name} by \`${rk}\`; Seneca carries it as \`id\`.
|
|
1715
|
-
|
|
1716
|
-
|
|
1273
|
+
// The key goes on the body under the API's own name, where the route
|
|
1274
|
+
// reads it, and into the match, which the SDK consults first.
|
|
1275
|
+
let key = null
|
|
1276
|
+
if (null != data.id) {
|
|
1277
|
+
key = { ${jsKey(rk)}: data.id }
|
|
1278
|
+
if (null == ${jsProp('data', rk)}) {
|
|
1279
|
+
${jsProp('data', rk)} = data.id
|
|
1280
|
+
}
|
|
1717
1281
|
}
|
|
1718
|
-
|
|
1719
|
-
// \`${
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
|
|
1724
|
-
delete ${jsProp('data', provider.lower + '_id')}
|
|
1282
|
+
${dropPark}
|
|
1283
|
+
// NOR DOES SENECA'S \`id\`. It holds the \`${rk}\` this API addresses
|
|
1284
|
+
// the record by, under a name this API's ${e.name} does not have: sent
|
|
1285
|
+
// on the body it is at best an unknown property, and to a store that
|
|
1286
|
+
// matches a write against the record it names the wrong one.
|
|
1287
|
+
delete data.id
|
|
1725
1288
|
`
|
|
1726
1289
|
|
|
1290
|
+
const saveVals = keyed ? 'key' : undefined
|
|
1291
|
+
|
|
1292
|
+
const call = (opname: string) =>
|
|
1293
|
+
`await sdk.${e.acc}(${entArg}).${opname}(data)`
|
|
1294
|
+
|
|
1295
|
+
const body = hasCreate && hasUpdate
|
|
1296
|
+
? (!refuseUpdate ? ` let res
|
|
1297
|
+
if (${isUpdate}) {
|
|
1298
|
+
${guard('update', 'data', 'save', ' ')} res = ${call('update')}
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
${guard('create', 'data', 'save', ' ')} res = ${call('create')}
|
|
1302
|
+
}` : ` if (${isUpdate}) {
|
|
1303
|
+
${refuse('update', ' ')} }
|
|
1304
|
+
|
|
1305
|
+
${guard('create', 'data', 'save')} const res = ${call('create')}`)
|
|
1306
|
+
: hasCreate
|
|
1307
|
+
? `${guard('create', 'data', 'save')} const res = ${call('create')}`
|
|
1308
|
+
: `${refuse('update')}${guard('update', 'data', 'save')} const res = ${call('update')}`
|
|
1309
|
+
|
|
1727
1310
|
Content(`
|
|
1728
1311
|
${jsProp('entity', e.name)}.cmd.save.action =
|
|
1729
1312
|
async function save_${e.name}(this: any, entize: any, msg: any) {
|
|
1730
1313
|
const data = msg.ent.data$(false)
|
|
1731
|
-
${
|
|
1314
|
+
${keyBlock} const sdk = this.shared.sdk
|
|
1732
1315
|
|
|
1733
1316
|
${actionBranch('save',
|
|
1734
1317
|
` // The action's OWN payload is the entity's own fields — data$(false)
|
|
1735
1318
|
// has already dropped every trailing-\`$\` key, \`action$\` included,
|
|
1736
1319
|
// so \`$action\` is the only thing added here.
|
|
1737
1320
|
data.$action = action$
|
|
1738
|
-
const done = await sdk.${e.acc}()[op$](data)
|
|
1739
|
-
return entize(${out('done')})
|
|
1740
|
-
`)}${
|
|
1321
|
+
const done = await sdk.${e.acc}(${entArg})[op$](data)
|
|
1322
|
+
return entize(${out('done', saveVals)})
|
|
1323
|
+
`)}${body}
|
|
1741
1324
|
|
|
1742
|
-
return entize(${out('res',
|
|
1325
|
+
return entize(${out('res', saveVals)})
|
|
1743
1326
|
}
|
|
1744
1327
|
|
|
1745
1328
|
`)
|
|
1746
1329
|
}
|
|
1747
1330
|
|
|
1748
1331
|
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
1332
|
Content(`
|
|
1764
1333
|
${jsProp('entity', e.name)}.cmd.remove.action =
|
|
1765
1334
|
async function remove_${e.name}(this: any, entize: any, msg: any) {
|
|
1766
1335
|
const q = cleanq(msg.q)
|
|
1767
1336
|
${actionBranch('remove',
|
|
1768
|
-
` const gone = await ornull(() => this.shared.sdk.${e.acc}()[op$](
|
|
1337
|
+
` const gone = await ornull(() => this.shared.sdk.${e.acc}()[op$](${aq}))
|
|
1769
1338
|
return null == gone ? null : entize(${out('gone')})
|
|
1770
1339
|
`)}${'' !== refuse('remove') ? refuse('remove') :
|
|
1771
1340
|
`${guard('remove', 'q')}${splitLine('remove')} await ornull(() => this.shared.sdk.${e.acc}().remove(${sdkArg('remove')}))
|
|
@@ -1883,7 +1452,6 @@ if ('undefined' !== typeof module) {
|
|
|
1883
1452
|
})
|
|
1884
1453
|
|
|
1885
1454
|
|
|
1886
|
-
// --- src/<Name>Provider-doc.ts ----------------------------------------------
|
|
1887
1455
|
|
|
1888
1456
|
const ProviderDoc = cmp(function ProviderDoc(props: any) {
|
|
1889
1457
|
const { provider } = props
|
|
@@ -1926,6 +1494,7 @@ if ('undefined' !== typeof module) {
|
|
|
1926
1494
|
export {
|
|
1927
1495
|
Main,
|
|
1928
1496
|
recordKey,
|
|
1497
|
+
rkOnCreate,
|
|
1929
1498
|
cmdActions,
|
|
1930
1499
|
entityActionList,
|
|
1931
1500
|
}
|