@enrichlayer/el-linear 1.34.0 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils/linear-service.js +27 -19
- package/dist/utils/output.js +87 -5
- package/package.json +1 -1
|
@@ -131,20 +131,32 @@ export class LinearService {
|
|
|
131
131
|
else if (options.excludeStates && options.excludeStates.length > 0) {
|
|
132
132
|
filter.state = { nin: options.excludeStates };
|
|
133
133
|
}
|
|
134
|
-
if (options.teamId) {
|
|
135
|
-
filter.teams = { some: { id: { eq: options.teamId } } };
|
|
136
|
-
}
|
|
137
134
|
// `limit === 0` means "no limit" — paginate the full result set.
|
|
138
135
|
// Otherwise a single page of `limit` projects. DEV-4175: `--all` /
|
|
139
136
|
// `--limit 0` must return every project so callers never make a false
|
|
140
137
|
// "does not exist" determination off a silently truncated page.
|
|
141
138
|
const unlimited = limit === 0;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
// DEV-5325: ProjectFilter has no `teams` relation filter — the
|
|
140
|
+
// `teams: { some: … }` shape this used since DEV-4165 errors against
|
|
141
|
+
// the live GraphQL schema ('Field "teams" is not defined by type
|
|
142
|
+
// "ProjectFilter"'). Team scoping queries from the team side instead:
|
|
143
|
+
// `Team.projects` accepts the same name/state ProjectFilter and
|
|
144
|
+
// paginates identically, keeping the filter server-side. (Args stay
|
|
145
|
+
// inline in each branch so `sdkOrderBy`'s generic keeps its contextual
|
|
146
|
+
// type from the SDK signature.)
|
|
147
|
+
let page = options.teamId
|
|
148
|
+
? await (await this.client.team(options.teamId)).projects({
|
|
149
|
+
filter: nonEmptyFilter(filter),
|
|
150
|
+
first: unlimited ? 250 : limit,
|
|
151
|
+
orderBy: sdkOrderBy("updatedAt"),
|
|
152
|
+
includeArchived: false,
|
|
153
|
+
})
|
|
154
|
+
: await this.client.projects({
|
|
155
|
+
filter: nonEmptyFilter(filter),
|
|
156
|
+
first: unlimited ? 250 : limit,
|
|
157
|
+
orderBy: sdkOrderBy("updatedAt"),
|
|
158
|
+
includeArchived: false,
|
|
159
|
+
});
|
|
148
160
|
const projectNodes = [...page.nodes];
|
|
149
161
|
if (unlimited) {
|
|
150
162
|
while (page.pageInfo.hasNextPage) {
|
|
@@ -516,25 +528,21 @@ export class LinearService {
|
|
|
516
528
|
throw notFoundError("Project", projectInput);
|
|
517
529
|
}
|
|
518
530
|
// DEV-4103: scope by team when provided so a name shared across teams
|
|
519
|
-
// doesn't resolve to a different team's project.
|
|
520
|
-
// `
|
|
531
|
+
// doesn't resolve to a different team's project. DEV-5325: the scoping
|
|
532
|
+
// goes through `Team.projects` (same as `getProjects`) — ProjectFilter
|
|
533
|
+
// rejects a `teams` relation filter against the live schema.
|
|
521
534
|
const teamId = teamInput ? await this.resolveTeamId(teamInput) : undefined;
|
|
522
535
|
const filter = {
|
|
523
536
|
name: { eqIgnoreCase: projectInput },
|
|
524
537
|
};
|
|
525
|
-
if (teamId) {
|
|
526
|
-
// Matches the filter shape `getProjects` uses for `--team`.
|
|
527
|
-
filter.teams = { some: { id: { eq: teamId } } };
|
|
528
|
-
}
|
|
529
538
|
// `first: 5` is wide enough to detect ambiguity (same name across
|
|
530
539
|
// multiple teams) without paying for a deeper page. Linear's UI caps
|
|
531
540
|
// effective project-name collisions at a handful in practice; if a
|
|
532
541
|
// workspace ever exceeds this we'll see it as a still-ambiguous error
|
|
533
542
|
// listing the first 5 teams — better than a silent wrong pick.
|
|
534
|
-
const projectsConnection =
|
|
535
|
-
filter,
|
|
536
|
-
first: 5
|
|
537
|
-
});
|
|
543
|
+
const projectsConnection = teamId
|
|
544
|
+
? await (await this.client.team(teamId)).projects({ filter, first: 5 })
|
|
545
|
+
: await this.client.projects({ filter, first: 5 });
|
|
538
546
|
if (projectsConnection.nodes.length === 0) {
|
|
539
547
|
const context = teamInput ? `on team "${teamInput}"` : undefined;
|
|
540
548
|
throw notFoundError("Project", projectInput, context);
|
package/dist/utils/output.js
CHANGED
|
@@ -40,9 +40,58 @@ export function setOutputFormat(format) {
|
|
|
40
40
|
export function getOutputFormat() {
|
|
41
41
|
return outputFormat;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a dot-separated path against a value (`a.b.c`; array indices are
|
|
45
|
+
* numeric segments, `items.0.id`). Returns `undefined` when any segment is
|
|
46
|
+
* missing so callers can distinguish "missing" from a real `null` value.
|
|
47
|
+
* Same path grammar as el-git's `--field` selector — keeping the two flags'
|
|
48
|
+
* semantics aligned is the point (DEV-5323).
|
|
49
|
+
*/
|
|
50
|
+
function getNestedPath(obj, path) {
|
|
51
|
+
let cur = obj;
|
|
52
|
+
for (const seg of path.split(".")) {
|
|
53
|
+
if (cur === null || cur === undefined || typeof cur !== "object") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
cur = cur[seg];
|
|
57
|
+
}
|
|
58
|
+
return cur;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Project `obj` down to the requested fields.
|
|
62
|
+
*
|
|
63
|
+
* - Top-level keys copy through as before.
|
|
64
|
+
* - Dot-separated paths (DEV-5323) resolve nested values; the requested path
|
|
65
|
+
* string becomes a flat output key (`{"pipeline.status": "success"}`), so
|
|
66
|
+
* consumers read exactly what they asked for.
|
|
67
|
+
* - A field that resolves nowhere is emitted as an explicit `null` AND
|
|
68
|
+
* reported via `unresolved` — never silently omitted. Silent omission made
|
|
69
|
+
* a typo'd field indistinguishable from an empty value (DEV-5323).
|
|
70
|
+
* - For arrays, each item is projected; a field counts as unresolved only
|
|
71
|
+
* when it resolves on NO item (heterogeneous lists legitimately have
|
|
72
|
+
* per-item gaps).
|
|
73
|
+
*/
|
|
74
|
+
function filterFields(obj, fields, unresolved) {
|
|
44
75
|
if (Array.isArray(obj)) {
|
|
45
|
-
|
|
76
|
+
const resolvedSomewhere = new Set();
|
|
77
|
+
const items = obj.map((item) => {
|
|
78
|
+
const perItem = new Set();
|
|
79
|
+
const projected = filterFields(item, fields, perItem);
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
if (!perItem.has(field)) {
|
|
82
|
+
resolvedSomewhere.add(field);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return projected;
|
|
86
|
+
});
|
|
87
|
+
if (unresolved) {
|
|
88
|
+
for (const field of fields) {
|
|
89
|
+
if (!resolvedSomewhere.has(field)) {
|
|
90
|
+
unresolved.add(field);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return items;
|
|
46
95
|
}
|
|
47
96
|
if (obj !== null && typeof obj === "object") {
|
|
48
97
|
const source = obj;
|
|
@@ -50,7 +99,17 @@ function filterFields(obj, fields) {
|
|
|
50
99
|
for (const field of fields) {
|
|
51
100
|
if (field in source) {
|
|
52
101
|
result[field] = source[field];
|
|
102
|
+
continue;
|
|
53
103
|
}
|
|
104
|
+
const nested = field.includes(".")
|
|
105
|
+
? getNestedPath(source, field)
|
|
106
|
+
: undefined;
|
|
107
|
+
if (nested !== undefined) {
|
|
108
|
+
result[field] = nested;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
result[field] = null;
|
|
112
|
+
unresolved?.add(field);
|
|
54
113
|
}
|
|
55
114
|
return result;
|
|
56
115
|
}
|
|
@@ -143,18 +202,41 @@ export function outputSuccess(data) {
|
|
|
143
202
|
// (project.name, teams[].key) the formatter needs to render. Skip the
|
|
144
203
|
// JSON filter when summary is active and let the formatter do the work.
|
|
145
204
|
if (fieldsFilter && outputFormat !== "summary") {
|
|
205
|
+
const unresolved = new Set();
|
|
146
206
|
if (Array.isArray(output)) {
|
|
147
|
-
output = filterFields(output, fieldsFilter);
|
|
207
|
+
output = filterFields(output, fieldsFilter, unresolved);
|
|
148
208
|
}
|
|
149
209
|
else if (output !== null && typeof output === "object") {
|
|
150
210
|
const obj = output;
|
|
151
211
|
if (Array.isArray(obj.data)) {
|
|
152
|
-
output = {
|
|
212
|
+
output = {
|
|
213
|
+
...obj,
|
|
214
|
+
data: filterFields(obj.data, fieldsFilter, unresolved),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
else if (obj.data !== null &&
|
|
218
|
+
obj.data !== undefined &&
|
|
219
|
+
typeof obj.data === "object") {
|
|
220
|
+
// Envelope with an OBJECT data payload (e.g. el-git context):
|
|
221
|
+
// project inside `data`, same as the array branch. Before
|
|
222
|
+
// DEV-5323 this fell through to root filtering, so
|
|
223
|
+
// `--fields branch,issueId` on an envelope returned `{}`.
|
|
224
|
+
// Paths are relative to `data` for every envelope shape.
|
|
225
|
+
output = {
|
|
226
|
+
...obj,
|
|
227
|
+
data: filterFields(obj.data, fieldsFilter, unresolved),
|
|
228
|
+
};
|
|
153
229
|
}
|
|
154
230
|
else {
|
|
155
|
-
output = filterFields(output, fieldsFilter);
|
|
231
|
+
output = filterFields(output, fieldsFilter, unresolved);
|
|
156
232
|
}
|
|
157
233
|
}
|
|
234
|
+
if (unresolved.size > 0) {
|
|
235
|
+
// Fail-visible: the projected keys carry explicit nulls and this
|
|
236
|
+
// warning names them, so a typo'd/missing field is never mistaken
|
|
237
|
+
// for an empty value (DEV-5323).
|
|
238
|
+
outputWarning(`fields_unresolved: ${[...unresolved].join(", ")} did not resolve on this payload (emitted as null). Paths are dot-separated and relative to the envelope's data when one is present.`);
|
|
239
|
+
}
|
|
158
240
|
}
|
|
159
241
|
// summary format takes the post-raw value and renders it as a
|
|
160
242
|
// human-readable block. We bypass the jq path because jq is a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.1",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|