@bakers-build/addon-name-refs 0.1.2-next.93 → 0.2.0
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/dist/src/index.js +200 -148
- package/package.json +2 -2
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// BB-403: resolve entity NAMES into the relationship that references them, so
|
|
2
|
-
// a skill writes `--
|
|
2
|
+
// a skill writes `--type_names bug` or `--environment_names staging`
|
|
3
3
|
// instead of searching the collection, pulling a UUID out with jq, and
|
|
4
4
|
// hand-building the linkage.
|
|
5
5
|
//
|
|
@@ -19,28 +19,31 @@
|
|
|
19
19
|
// committed config already has is unchanged. It contributes no preset
|
|
20
20
|
// accumulator, so it is a plain sibling edge in `PERMITTED_SIBLINGS`.
|
|
21
21
|
//
|
|
22
|
-
// Adding a target is one RESOLVERS entry.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
22
|
+
// Adding a target is one RESOLVERS entry. Both kinds of name are unique only
|
|
23
|
+
// within a parent. `bb_environment` names are unique WITHIN A PROJECT
|
|
24
|
+
// (BbEnvironmentUniquePerProject), which the write carries directly — hence
|
|
25
|
+
// `scopeField`. `bb_term` names (ticket type, recipe, label) are unique within
|
|
26
|
+
// the ticket's WORKSPACE, reached through `scopeChain`; a term name missing
|
|
27
|
+
// there is created there, so an agent never needs a lead to add vocabulary.
|
|
28
|
+
/** A term resolver: workspace-scoped, created on first use. */
|
|
29
|
+
const term = (field, noun, relationship, vid, single) => ({
|
|
30
|
+
field,
|
|
31
|
+
noun,
|
|
32
|
+
relationship,
|
|
33
|
+
type: `bb_term--${vid}`,
|
|
34
|
+
path: `bb_term/${vid}`,
|
|
35
|
+
title: `${noun[0]?.toUpperCase()}${noun.slice(1)} names (comma-separated ${vid} term names` +
|
|
36
|
+
`${single ? '; one name' : ''}; resolved in the ticket's workspace and ` +
|
|
37
|
+
'created there when missing, on the live write)',
|
|
38
|
+
scopeField: 'workspace_id',
|
|
39
|
+
scopeChain: 'project_id.group_id.workspace_id',
|
|
40
|
+
single,
|
|
41
|
+
});
|
|
42
|
+
const WORKSPACE_TYPE = 'bb_workspace--bb_workspace';
|
|
28
43
|
const RESOLVERS = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
relationship: 'labels',
|
|
33
|
-
type: 'bb_term--bb_labels',
|
|
34
|
-
path: 'bb_term/bb_labels',
|
|
35
|
-
title: 'Label names (comma-separated bb_labels names; ' +
|
|
36
|
-
'unique only within workspace_id — resolved to labels on the live write)',
|
|
37
|
-
// BB-531: bb_term names are unique within (workspace_id, vid), so the
|
|
38
|
-
// same name legitimately exists in several workspaces and a bare index
|
|
39
|
-
// would silently pick one. The workspace is reached from the payload via
|
|
40
|
-
// the chain project_id → group_id → workspace_id.
|
|
41
|
-
scopeField: 'workspace_id',
|
|
42
|
-
scopeChain: 'project_id.group_id.workspace_id',
|
|
43
|
-
},
|
|
44
|
+
term('type_names', 'ticket type', 'ticket_type', 'type', true),
|
|
45
|
+
term('recipe_names', 'recipe', 'recipe', 'recipe', true),
|
|
46
|
+
term('label_names', 'label', 'labels', 'label', false),
|
|
44
47
|
{
|
|
45
48
|
field: 'environment_names',
|
|
46
49
|
noun: 'environment',
|
|
@@ -65,7 +68,7 @@ const MAX_PAGES = 10;
|
|
|
65
68
|
/** Rows this resolver will walk before it refuses to resolve at all. */
|
|
66
69
|
const MAX_ROWS = PAGE_LIMIT * MAX_PAGES;
|
|
67
70
|
/**
|
|
68
|
-
* How many known names an error may list.
|
|
71
|
+
* How many known names an error may list. A term bundle is small; a future
|
|
69
72
|
* target need not be, and an error nobody can read is an error nobody reads.
|
|
70
73
|
*/
|
|
71
74
|
const NAME_LIST_CAP = 20;
|
|
@@ -185,12 +188,12 @@ async function resolveChainScope(req, ctx, ref, data) {
|
|
|
185
188
|
}
|
|
186
189
|
/**
|
|
187
190
|
* Does this operation schema declare this resolver's relationship over its
|
|
188
|
-
* target type
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
+
* target type, in the cardinality the resolver writes? A single resolver
|
|
192
|
+
* writes one object, a multi-valued one an array; matching the other shape
|
|
193
|
+
* would produce an invalid payload.
|
|
191
194
|
*/
|
|
192
195
|
function declares(schema, ref) {
|
|
193
|
-
const targets = at(schema, 'properties', 'data', 'properties', 'relationships', 'properties', ref.relationship, 'properties', 'data', 'items', 'properties', 'type', 'enum');
|
|
196
|
+
const targets = at(schema, 'properties', 'data', 'properties', 'relationships', 'properties', ref.relationship, 'properties', 'data', ...(ref.single ? [] : ['items']), 'properties', 'type', 'enum');
|
|
194
197
|
return Array.isArray(targets) && targets.includes(ref.type);
|
|
195
198
|
}
|
|
196
199
|
/** `/jsonapi`, however the config spelled it — with, without, or with a trailing slash. */
|
|
@@ -210,10 +213,19 @@ function normalisePrefix(raw) {
|
|
|
210
213
|
* of handing an HTML error page to `JSON.parse`.
|
|
211
214
|
*/
|
|
212
215
|
async function fetchDoc(ctx, url, what) {
|
|
216
|
+
return sendDoc(ctx, { method: 'GET', url }, `could not read ${what}`);
|
|
217
|
+
}
|
|
218
|
+
/** Send one JSON:API request and parse its document, or fail by name. */
|
|
219
|
+
async function sendDoc(ctx, req, failure) {
|
|
220
|
+
const url = req.url;
|
|
213
221
|
const res = await ctx.http.send(await ctx.auth.apply({
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
...req,
|
|
223
|
+
headers: {
|
|
224
|
+
Accept: 'application/vnd.api+json',
|
|
225
|
+
...(req.body === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { 'Content-Type': 'application/vnd.api+json' }),
|
|
228
|
+
},
|
|
217
229
|
}));
|
|
218
230
|
if (res.status < 200 || res.status >= 300) {
|
|
219
231
|
const expiry = res.status === 401 || res.status === 403
|
|
@@ -221,15 +233,37 @@ async function fetchDoc(ctx, url, what) {
|
|
|
221
233
|
'and, unlike dropsh’s JSON:API client, cannot renew one, so an expired ' +
|
|
222
234
|
'token surfaces here instead of being refreshed. Re-authenticate and retry.'
|
|
223
235
|
: '';
|
|
224
|
-
throw new Error(
|
|
236
|
+
throw new Error(`${failure}: HTTP ${res.status} from ${url}.${expiry}`);
|
|
225
237
|
}
|
|
226
238
|
try {
|
|
227
239
|
return JSON.parse(res.body);
|
|
228
240
|
}
|
|
229
241
|
catch (cause) {
|
|
230
|
-
throw new Error(
|
|
242
|
+
throw new Error(`${failure}: ${url} answered ${res.status} but not JSON (${String(cause)})`);
|
|
231
243
|
}
|
|
232
244
|
}
|
|
245
|
+
/** Create a term in a workspace and return its uuid. */
|
|
246
|
+
async function createTerm(ctx, ref, name, workspaceId) {
|
|
247
|
+
const prefix = normalisePrefix(ctx.jsonapiPrefix);
|
|
248
|
+
const doc = await sendDoc(ctx, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
url: `${ctx.baseUrl.replace(/\/+$/, '')}${prefix}/${ref.path}`,
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
data: {
|
|
253
|
+
type: ref.type,
|
|
254
|
+
attributes: { name },
|
|
255
|
+
relationships: {
|
|
256
|
+
workspace_id: { data: { type: WORKSPACE_TYPE, id: workspaceId } },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
}, `could not create ${ref.noun} '${name}' in workspace '${workspaceId}'`);
|
|
261
|
+
const id = at(doc, 'data', 'id');
|
|
262
|
+
if (typeof id !== 'string') {
|
|
263
|
+
throw new Error(`could not create ${ref.noun} '${name}': the response carries no id`);
|
|
264
|
+
}
|
|
265
|
+
return id;
|
|
266
|
+
}
|
|
233
267
|
/**
|
|
234
268
|
* The whole collection as name → candidates. One round trip regardless of how
|
|
235
269
|
* many names were given, and it yields the known-names list the error needs.
|
|
@@ -289,15 +323,113 @@ function describe(candidates) {
|
|
|
289
323
|
.map((c) => (c.scopeId === undefined ? c.id : `${c.id} (${c.scopeId})`))
|
|
290
324
|
.join(', ');
|
|
291
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* One term name → its uuid in the ticket's workspace, created there when
|
|
328
|
+
* missing. Without a workspace (a create naming no project) nothing can be
|
|
329
|
+
* created and only an unambiguous existing name resolves.
|
|
330
|
+
*/
|
|
331
|
+
async function resolveTerm(ctx, ref, byName, name, workspaceId) {
|
|
332
|
+
const candidates = byName.get(name) ?? [];
|
|
333
|
+
if (workspaceId === undefined) {
|
|
334
|
+
const [only, ...rivals] = candidates;
|
|
335
|
+
if (only === undefined) {
|
|
336
|
+
throw new Error(`${ref.noun} name '${name}' does not exist in the ${ref.path} ` +
|
|
337
|
+
`vocabulary, and this write names no project_id whose workspace it ` +
|
|
338
|
+
`could be created in. Known names: ${knownNames(byName)}`);
|
|
339
|
+
}
|
|
340
|
+
if (rivals.length > 0) {
|
|
341
|
+
throw new Error(`${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
342
|
+
`entries in ${ref.path} share it. Candidates: ${describe(candidates)}. ` +
|
|
343
|
+
`Pass the uuid directly on the ${ref.relationship} relationship instead.`);
|
|
344
|
+
}
|
|
345
|
+
return only.id;
|
|
346
|
+
}
|
|
347
|
+
const [match, ...extra] = candidates.filter((c) => c.scopeId === workspaceId);
|
|
348
|
+
if (extra.length > 0 && match !== undefined) {
|
|
349
|
+
throw new Error(`${ref.noun} name '${name}' is ambiguous even within workspace_id ` +
|
|
350
|
+
`'${workspaceId}': ${describe([match, ...extra])}. Pass the uuid ` +
|
|
351
|
+
`directly on the ${ref.relationship} relationship instead.`);
|
|
352
|
+
}
|
|
353
|
+
if (match !== undefined)
|
|
354
|
+
return match.id;
|
|
355
|
+
const id = await createTerm(ctx, ref, name, workspaceId);
|
|
356
|
+
byName.set(name, [...candidates, { id, scopeId: workspaceId }]);
|
|
357
|
+
return id;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* One name → its uuid within the parent the write carries (`scopeField`),
|
|
361
|
+
* read off the entity under update when the payload omits it.
|
|
362
|
+
*/
|
|
363
|
+
async function resolveScoped(req, ctx, ref, data, byName, name, scopes) {
|
|
364
|
+
const candidates = byName.get(name) ?? [];
|
|
365
|
+
const [only, ...rivals] = candidates;
|
|
366
|
+
if (only === undefined) {
|
|
367
|
+
throw new Error(`${ref.noun} name '${name}' does not exist in the ${ref.path} ` +
|
|
368
|
+
`vocabulary. Known names: ${knownNames(byName)}`);
|
|
369
|
+
}
|
|
370
|
+
const scopeField = ref.scopeField;
|
|
371
|
+
if (scopeField === undefined) {
|
|
372
|
+
if (rivals.length > 0) {
|
|
373
|
+
throw new Error(`${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
374
|
+
`entries in ${ref.path} share it. Candidates: ${describe(candidates)}. ` +
|
|
375
|
+
`Pass the uuid directly on the ${ref.relationship} relationship instead.`);
|
|
376
|
+
}
|
|
377
|
+
return only.id;
|
|
378
|
+
}
|
|
379
|
+
// A scoped resolver ALWAYS establishes a scope — including for a name that
|
|
380
|
+
// is unique across the whole collection today. Uniqueness is a property of
|
|
381
|
+
// the current rows, not of the name: `staging` is unique right up to the
|
|
382
|
+
// moment a second project creates one, and until then an unscoped write
|
|
383
|
+
// would attach another project's environment and report success. The scope
|
|
384
|
+
// the payload carries is free; otherwise it is read off the entity being
|
|
385
|
+
// updated (memoised: one read per write, however many names were given). A
|
|
386
|
+
// create has no entity yet, hence `undefined`.
|
|
387
|
+
let scopeId = relationshipId(data, scopeField);
|
|
388
|
+
if (scopeId === undefined) {
|
|
389
|
+
let pending = scopes.get(scopeField);
|
|
390
|
+
if (pending === undefined) {
|
|
391
|
+
pending =
|
|
392
|
+
ctx.operation === 'update'
|
|
393
|
+
? fetchScope(req, ctx, scopeField)
|
|
394
|
+
: Promise.resolve(undefined);
|
|
395
|
+
scopes.set(scopeField, pending);
|
|
396
|
+
}
|
|
397
|
+
scopeId = await pending;
|
|
398
|
+
}
|
|
399
|
+
if (scopeId === undefined) {
|
|
400
|
+
throw new Error(rivals.length > 0
|
|
401
|
+
? `${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
402
|
+
`entries in ${ref.path} share it and the write carries no ` +
|
|
403
|
+
`${scopeField} to narrow them. Candidates: ${describe(candidates)}. ` +
|
|
404
|
+
`Pass the uuid directly on the ${ref.relationship} relationship instead.`
|
|
405
|
+
: `${ref.noun} name '${name}' cannot be resolved: ${ref.path} names are ` +
|
|
406
|
+
`unique only within ${scopeField}, and this write carries no ` +
|
|
407
|
+
`${scopeField} to narrow it — being the only '${name}' that exists ` +
|
|
408
|
+
`today does not make it this write's. Candidate: ${describe(candidates)}. ` +
|
|
409
|
+
`Name the ${scopeField}, or pass the uuid directly on the ` +
|
|
410
|
+
`${ref.relationship} relationship instead.`);
|
|
411
|
+
}
|
|
412
|
+
const [match, ...extra] = candidates.filter((c) => c.scopeId === scopeId);
|
|
413
|
+
if (match === undefined) {
|
|
414
|
+
const elsewhere = candidates.map((c) => c.scopeId ?? '(none)').join(', ');
|
|
415
|
+
throw new Error(`${ref.noun} name '${name}' does not exist in ${scopeField} ` +
|
|
416
|
+
`'${scopeId}'. It exists in: ${elsewhere}.`);
|
|
417
|
+
}
|
|
418
|
+
if (extra.length > 0) {
|
|
419
|
+
throw new Error(`${ref.noun} name '${name}' is ambiguous even within ${scopeField} ` +
|
|
420
|
+
`'${scopeId}': ${describe([match, ...extra])}. Pass the uuid directly ` +
|
|
421
|
+
`on the ${ref.relationship} relationship instead.`);
|
|
422
|
+
}
|
|
423
|
+
return match.id;
|
|
424
|
+
}
|
|
292
425
|
export function nameRefsPlugin() {
|
|
293
426
|
// Per-instance, not module-level: one CLI invocation performs one write, so
|
|
294
427
|
// these guard against a second lookup, and they keep tests isolated.
|
|
295
428
|
const indexes = new Map();
|
|
296
429
|
const scopes = new Map();
|
|
297
430
|
/**
|
|
298
|
-
* Chain-scope resolutions, keyed by the
|
|
299
|
-
*
|
|
300
|
-
* a chain resolution is a round trip we want to pay once per write.
|
|
431
|
+
* Chain-scope resolutions, keyed by the chain: the three term resolvers share
|
|
432
|
+
* one, and a chain resolution is a round trip we want to pay once per write.
|
|
301
433
|
*/
|
|
302
434
|
const chainScopes = new Map();
|
|
303
435
|
/**
|
|
@@ -381,6 +513,18 @@ export function nameRefsPlugin() {
|
|
|
381
513
|
? { ...data.relationships }
|
|
382
514
|
: {};
|
|
383
515
|
const declaredHere = offered.get(bundleKey(ctx.entityType, ctx.bundle));
|
|
516
|
+
const namesOf = (ref) => [
|
|
517
|
+
...new Set(String(attributes[ref.field])
|
|
518
|
+
.split(',')
|
|
519
|
+
.map((name) => name.trim())
|
|
520
|
+
.filter((name) => name.length > 0)),
|
|
521
|
+
];
|
|
522
|
+
// Before any round trip: a single-valued field takes one name.
|
|
523
|
+
for (const ref of active) {
|
|
524
|
+
if (ref.single && namesOf(ref).length > 1) {
|
|
525
|
+
throw new Error(`'${ref.field}' takes one name: ${ref.relationship} holds a single ${ref.noun}.`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
384
528
|
for (const ref of active) {
|
|
385
529
|
// A name field on a bundle that has no such relationship. Reachable by
|
|
386
530
|
// a hand-built body or `--no-validate`, since the flag itself only
|
|
@@ -393,10 +537,7 @@ export function nameRefsPlugin() {
|
|
|
393
537
|
`Remove it, or write to a bundle that has ${ref.relationship}.`);
|
|
394
538
|
}
|
|
395
539
|
delete nextAttributes[ref.field];
|
|
396
|
-
const names =
|
|
397
|
-
.split(',')
|
|
398
|
-
.map((name) => name.trim())
|
|
399
|
-
.filter((name) => name.length > 0);
|
|
540
|
+
const names = namesOf(ref);
|
|
400
541
|
if (names.length === 0)
|
|
401
542
|
continue;
|
|
402
543
|
let index = indexes.get(ref.field);
|
|
@@ -405,120 +546,31 @@ export function nameRefsPlugin() {
|
|
|
405
546
|
indexes.set(ref.field, index);
|
|
406
547
|
}
|
|
407
548
|
const byName = await index;
|
|
408
|
-
const scopeField = ref.scopeField;
|
|
409
|
-
// The scope the payload already carries. Free — no round trip — so it
|
|
410
|
-
// narrows EVERY name, including an unambiguous one: a name that is
|
|
411
|
-
// unique across the whole collection can still belong to a different
|
|
412
|
-
// parent than the entity being written, and attaching it silently would
|
|
413
|
-
// be worse than the round trip we are saving. This shortcut only
|
|
414
|
-
// applies when the payload carries the SAME relationship the target's
|
|
415
|
-
// `scopeField` names; a chain resolver reaches the scope via a fetch
|
|
416
|
-
// and never gets a free read here.
|
|
417
|
-
const payloadScope = scopeField === undefined || ref.scopeChain !== undefined
|
|
418
|
-
? undefined
|
|
419
|
-
: relationshipId(data, scopeField);
|
|
420
549
|
const resolved = [];
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (scopeField === undefined) {
|
|
429
|
-
if (rivals.length > 0) {
|
|
430
|
-
throw new Error(`${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
431
|
-
`entries in ${ref.path} share it. Candidates: ${describe(candidates)}. ` +
|
|
432
|
-
`Pass the uuid directly on the ${ref.relationship} relationship instead.`);
|
|
433
|
-
}
|
|
434
|
-
resolved.push(only.id);
|
|
435
|
-
continue;
|
|
550
|
+
if (ref.scopeChain !== undefined) {
|
|
551
|
+
// A term always belongs to the ticket's workspace: one fetch walks
|
|
552
|
+
// project → group → workspace, shared by all term fields of the write.
|
|
553
|
+
let pending = chainScopes.get(ref.scopeChain);
|
|
554
|
+
if (pending === undefined) {
|
|
555
|
+
pending = resolveChainScope(req, ctx, ref, data);
|
|
556
|
+
chainScopes.set(ref.scopeChain, pending);
|
|
436
557
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// right up to the moment a second project creates one, and until then
|
|
441
|
-
// an unscoped write would attach another project's environment and
|
|
442
|
-
// report success. A chain resolver reaches the scope via a single
|
|
443
|
-
// fetch (project + group + workspace in one round trip); a direct
|
|
444
|
-
// resolver reads the scope off the entity being updated (also
|
|
445
|
-
// memoised: one read per write, however many names were given). A
|
|
446
|
-
// create has no entity yet, hence `undefined`.
|
|
447
|
-
//
|
|
448
|
-
// A chain resolver fetches ONLY when there is something to narrow
|
|
449
|
-
// (multiple candidates). A single candidate needs no workspace check
|
|
450
|
-
// and skips the round trip — the unscoped-path fallback below
|
|
451
|
-
// resolves it directly. That keeps a write with one label and a
|
|
452
|
-
// `project_id` on the payload at one URL per resolver, matching the
|
|
453
|
-
// pre-fix cost.
|
|
454
|
-
let scopeId;
|
|
455
|
-
if (ref.scopeChain !== undefined) {
|
|
456
|
-
if (rivals.length > 0) {
|
|
457
|
-
let pending = chainScopes.get(ref.field);
|
|
458
|
-
if (pending === undefined) {
|
|
459
|
-
pending = resolveChainScope(req, ctx, ref, data);
|
|
460
|
-
chainScopes.set(ref.field, pending);
|
|
461
|
-
}
|
|
462
|
-
scopeId = await pending;
|
|
463
|
-
}
|
|
558
|
+
const workspaceId = await pending;
|
|
559
|
+
for (const name of names) {
|
|
560
|
+
resolved.push(await resolveTerm(ctx, ref, byName, name, workspaceId));
|
|
464
561
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (pending === undefined) {
|
|
470
|
-
pending =
|
|
471
|
-
ctx.operation === 'update'
|
|
472
|
-
? fetchScope(req, ctx, scopeField)
|
|
473
|
-
: Promise.resolve(undefined);
|
|
474
|
-
scopes.set(scopeField, pending);
|
|
475
|
-
}
|
|
476
|
-
scopeId = await pending;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// No scope at all is a hard error for a direct resolver (an
|
|
480
|
-
// environment name must belong to a project — never guess). A
|
|
481
|
-
// chain resolver falls back to the unscoped path instead: when no
|
|
482
|
-
// `project_id` can be derived, a single candidate resolves
|
|
483
|
-
// (backward-compatible with pre-fix writes) and multiple
|
|
484
|
-
// candidates raise the same "ambiguous" error an unscoped resolver
|
|
485
|
-
// would, with the candidate workspaces named via `describe()`.
|
|
486
|
-
if (scopeId === undefined) {
|
|
487
|
-
if (ref.scopeChain !== undefined) {
|
|
488
|
-
if (rivals.length > 0) {
|
|
489
|
-
throw new Error(`${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
490
|
-
`entries in ${ref.path} share it. Candidates: ${describe(candidates)}. ` +
|
|
491
|
-
`Pass the uuid directly on the ${ref.relationship} relationship instead.`);
|
|
492
|
-
}
|
|
493
|
-
resolved.push(only.id);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
throw new Error(rivals.length > 0
|
|
497
|
-
? `${ref.noun} name '${name}' is ambiguous — ${candidates.length} ` +
|
|
498
|
-
`entries in ${ref.path} share it and the write carries no ` +
|
|
499
|
-
`${scopeField} to narrow them. Candidates: ${describe(candidates)}. ` +
|
|
500
|
-
`Pass the uuid directly on the ${ref.relationship} relationship instead.`
|
|
501
|
-
: `${ref.noun} name '${name}' cannot be resolved: ${ref.path} names are ` +
|
|
502
|
-
`unique only within ${scopeField}, and this write carries no ` +
|
|
503
|
-
`${scopeField} to narrow it — being the only '${name}' that exists ` +
|
|
504
|
-
`today does not make it this write's. Candidate: ${describe(candidates)}. ` +
|
|
505
|
-
`Name the ${scopeField}, or pass the uuid directly on the ` +
|
|
506
|
-
`${ref.relationship} relationship instead.`);
|
|
507
|
-
}
|
|
508
|
-
const [match, ...extra] = candidates.filter((c) => c.scopeId === scopeId);
|
|
509
|
-
if (match === undefined) {
|
|
510
|
-
const elsewhere = candidates
|
|
511
|
-
.map((c) => c.scopeId ?? '(none)')
|
|
512
|
-
.join(', ');
|
|
513
|
-
throw new Error(`${ref.noun} name '${name}' does not exist in ${scopeField} ` +
|
|
514
|
-
`'${scopeId}'. It exists in: ${elsewhere}.`);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
for (const name of names) {
|
|
565
|
+
resolved.push(await resolveScoped(req, ctx, ref, data, byName, name, scopes));
|
|
515
566
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
567
|
+
}
|
|
568
|
+
if (ref.single) {
|
|
569
|
+
const [id] = resolved;
|
|
570
|
+
if (id !== undefined) {
|
|
571
|
+
relationships[ref.relationship] = { data: { type: ref.type, id } };
|
|
520
572
|
}
|
|
521
|
-
|
|
573
|
+
continue;
|
|
522
574
|
}
|
|
523
575
|
const current = at(relationships, ref.relationship, 'data');
|
|
524
576
|
const existing = Array.isArray(current)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/addon-name-refs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "bakers.build connection addon: resolve entity NAMES (--label_names, --environment_names) into the relationships that reference them, on the live write, via dropsh's extendOperationSchema and alterRequest hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"dropsh": "^0.7.5"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@bakers-build/core": "^0.
|
|
25
|
+
"@bakers-build/core": "^0.2.0"
|
|
26
26
|
}
|
|
27
27
|
}
|