@danypops/tickets 0.7.0 → 0.8.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/package.json +1 -1
- package/src/adapters/jira.ts +29 -0
- package/src/application/service.ts +20 -0
- package/src/cli/index.ts +2 -2
- package/src/config/config.ts +24 -0
- package/src/daemon/poller.ts +1 -1
- package/src/ports/repository.ts +17 -0
- package/src/vehicle/tickets-vehicle.ts +1 -1
package/package.json
CHANGED
package/src/adapters/jira.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface JiraBasicAuthOptions {
|
|
|
39
39
|
email: string;
|
|
40
40
|
token: string;
|
|
41
41
|
project?: string;
|
|
42
|
+
/** Additional project keys the poller's background sync also pools into the ledger, beyond the single default `project` above -- see buildSyncQuery(). */
|
|
43
|
+
syncProjects?: string[];
|
|
44
|
+
/** When true, the poller's background sync also pools everything assigned to the authenticated user (JQL `assignee = currentUser()`), regardless of project -- covers projects not listed in `project`/`syncProjects`. */
|
|
45
|
+
syncMine?: boolean;
|
|
42
46
|
timeoutMs?: number;
|
|
43
47
|
/** Injected in tests instead of a real network call — see axios's AxiosRequestConfig.adapter. */
|
|
44
48
|
axiosAdapter?: AxiosAdapter;
|
|
@@ -55,6 +59,8 @@ export interface JiraOAuthOptions {
|
|
|
55
59
|
accessToken: string;
|
|
56
60
|
cloudId: string;
|
|
57
61
|
project?: string;
|
|
62
|
+
syncProjects?: string[];
|
|
63
|
+
syncMine?: boolean;
|
|
58
64
|
timeoutMs?: number;
|
|
59
65
|
axiosAdapter?: AxiosAdapter;
|
|
60
66
|
configDir?: string;
|
|
@@ -125,6 +131,8 @@ export class JiraRepository {
|
|
|
125
131
|
/** Same auth/host config Version2Client was built from -- reused lazily by agileClient() so the Agile API client (board/quickfilter resolution) authenticates identically without duplicating the OAuth-vs-basic branching in the constructor below. */
|
|
126
132
|
private readonly clientConfig: JiraClientConfig;
|
|
127
133
|
private readonly project?: string;
|
|
134
|
+
private readonly syncProjects: string[];
|
|
135
|
+
private readonly syncMine: boolean;
|
|
128
136
|
private readonly configDir?: string;
|
|
129
137
|
/** display name (lowercased) -> { fieldId, schema type/items }, populated lazily from client.issueFields.getFields(). */
|
|
130
138
|
private customFieldCache?: Map<string, { id: string; type: string; items?: string }>;
|
|
@@ -136,6 +144,8 @@ export class JiraRepository {
|
|
|
136
144
|
constructor(name: string, opts: JiraOptions) {
|
|
137
145
|
this.name = name;
|
|
138
146
|
this.project = opts.project;
|
|
147
|
+
this.syncProjects = opts.syncProjects ?? [];
|
|
148
|
+
this.syncMine = opts.syncMine ?? false;
|
|
139
149
|
this.configDir = opts.configDir;
|
|
140
150
|
|
|
141
151
|
if (this.configDir) {
|
|
@@ -274,6 +284,25 @@ export class JiraRepository {
|
|
|
274
284
|
return this.searchJql(query, limit);
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
/**
|
|
288
|
+
* SyncScopeExpandable -- widens what the poller's own background sync pools
|
|
289
|
+
* into the local ledger beyond the single default `project` list() falls
|
|
290
|
+
* back to: every configured project (default plus syncProjects) ORed with
|
|
291
|
+
* "assignee = currentUser()" when syncMine is set, so issues assigned to
|
|
292
|
+
* you in a project nobody thought to list still get pooled. Returns
|
|
293
|
+
* undefined -- letting the poller fall back to plain list() -- when
|
|
294
|
+
* neither syncProjects nor syncMine adds anything beyond the default
|
|
295
|
+
* project's own existing behavior.
|
|
296
|
+
*/
|
|
297
|
+
buildSyncQuery(): string | undefined {
|
|
298
|
+
const projects = [...new Set([this.project, ...this.syncProjects].filter((p): p is string => Boolean(p)))];
|
|
299
|
+
if (projects.length <= 1 && !this.syncMine) return undefined;
|
|
300
|
+
const clauses: string[] = [];
|
|
301
|
+
if (projects.length > 0) clauses.push(`project in (${projects.map(jqlQuote).join(", ")})`);
|
|
302
|
+
if (this.syncMine) clauses.push("assignee = currentUser()");
|
|
303
|
+
return `${clauses.join(" OR ")} ORDER BY created DESC`;
|
|
304
|
+
}
|
|
305
|
+
|
|
277
306
|
/**
|
|
278
307
|
* BoardQuickFilterDiscoverable -- resolves a board's quick filter (the same object
|
|
279
308
|
* a board view's `quickFilter=` URL param and a backlog view's `customFilter=`
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
hasFieldDiscovery,
|
|
15
15
|
hasRawQuery,
|
|
16
16
|
hasStatusDiscovery,
|
|
17
|
+
hasSyncScopeExpansion,
|
|
17
18
|
hasTemplateDiscovery,
|
|
18
19
|
type IssueRepository,
|
|
19
20
|
} from "../ports/repository.js";
|
|
@@ -89,6 +90,25 @@ export class TicketService {
|
|
|
89
90
|
return this.repo(backend).list(filter);
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Fetches issues for the poller's own background sync pass (see
|
|
95
|
+
* daemon/poller.ts). Prefers a backend's own expanded sync scope
|
|
96
|
+
* (SyncScopeExpandable -- Jira: multiple configured projects plus
|
|
97
|
+
* everything assigned to the authenticated user, unioned into one query)
|
|
98
|
+
* over its plain default-project list() when one is configured; falls
|
|
99
|
+
* back to list() otherwise, so a backend with no sync scope configured
|
|
100
|
+
* (or with no such capability at all, e.g. GitHub/GitLab) behaves exactly
|
|
101
|
+
* as before.
|
|
102
|
+
*/
|
|
103
|
+
async syncFetch(backend: string, limit: number): Promise<Issue[]> {
|
|
104
|
+
const repo = this.repo(backend);
|
|
105
|
+
if (hasSyncScopeExpansion(repo) && hasRawQuery(repo)) {
|
|
106
|
+
const query = repo.buildSyncQuery();
|
|
107
|
+
if (query) return repo.runQuery(query, limit);
|
|
108
|
+
}
|
|
109
|
+
return repo.list({ limit });
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
async get(ref: string): Promise<Issue> {
|
|
93
113
|
const { backend, key } = parseRef(ref);
|
|
94
114
|
return this.repo(backend).get(key);
|
package/src/cli/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ program
|
|
|
40
40
|
.command("list")
|
|
41
41
|
.description("list issues on a backend")
|
|
42
42
|
.requiredOption("-b, --backend <name>", "backend name")
|
|
43
|
-
.option("--project <key>", "project key/id override (e.g. reach
|
|
43
|
+
.option("--project <key>", "project key/id override (e.g. reach ENG or OPS on a Jira backend defaulting to another project)")
|
|
44
44
|
.option("--status <status>", "filter by status")
|
|
45
45
|
.option("--assignee <user>", "filter by assignee")
|
|
46
46
|
.option("--label <label...>", "filter by label(s)")
|
|
@@ -109,7 +109,7 @@ program
|
|
|
109
109
|
.command("search <query>")
|
|
110
110
|
.description("search issues on a backend")
|
|
111
111
|
.requiredOption("-b, --backend <name>", "backend name")
|
|
112
|
-
.option("--project <key>", "project key/id override (e.g. reach
|
|
112
|
+
.option("--project <key>", "project key/id override (e.g. reach ENG or OPS on a Jira backend defaulting to another project)")
|
|
113
113
|
.option("--limit <n>", "max results", (v) => Number.parseInt(v, 10))
|
|
114
114
|
.action(async (query: string, opts) => {
|
|
115
115
|
await withClient((client) => client.call("issue.search", { backend: opts.backend, query, limit: opts.limit, project: opts.project }));
|
package/src/config/config.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface BackendConfig {
|
|
|
27
27
|
owner?: string;
|
|
28
28
|
project?: string;
|
|
29
29
|
repo?: string;
|
|
30
|
+
/** Jira only: additional project keys the background poller also pools into the ledger, beyond the single default `project` above. */
|
|
31
|
+
syncProjects?: string[];
|
|
32
|
+
/** Jira only: when true, the background poller also pools everything assigned to the authenticated user, regardless of project. */
|
|
33
|
+
syncMine?: boolean;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export interface Config {
|
|
@@ -58,6 +62,22 @@ function resolveToken(cfg: BackendConfig, env: NodeJS.ProcessEnv, envFallback: s
|
|
|
58
62
|
return env[envFallback];
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/** Config wins over the env var; the env var is a comma-separated list (JIRA_SYNC_PROJECTS=ENG,OPS). */
|
|
66
|
+
function resolveSyncProjects(cfg: BackendConfig, env: NodeJS.ProcessEnv): string[] | undefined {
|
|
67
|
+
if (cfg.syncProjects) return cfg.syncProjects;
|
|
68
|
+
const raw = env.JIRA_SYNC_PROJECTS;
|
|
69
|
+
if (!raw) return undefined;
|
|
70
|
+
return raw
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((p) => p.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveSyncMine(cfg: BackendConfig, env: NodeJS.ProcessEnv): boolean {
|
|
77
|
+
if (cfg.syncMine !== undefined) return cfg.syncMine;
|
|
78
|
+
return /^(1|true|yes)$/i.test(env.JIRA_SYNC_MINE ?? "");
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
/**
|
|
62
82
|
* Resolution order, highest priority first: (1) a running Enigma vault, if
|
|
63
83
|
* one happens to be configured for this backend — entirely optional, never a
|
|
@@ -208,6 +228,8 @@ async function createRepository(
|
|
|
208
228
|
accessToken: auth.token,
|
|
209
229
|
cloudId: auth.extra.cloudId,
|
|
210
230
|
project: cfg.project ?? env.JIRA_PROJECT,
|
|
231
|
+
syncProjects: resolveSyncProjects(cfg, env),
|
|
232
|
+
syncMine: resolveSyncMine(cfg, env),
|
|
211
233
|
configDir: configDir(),
|
|
212
234
|
});
|
|
213
235
|
}
|
|
@@ -219,6 +241,8 @@ async function createRepository(
|
|
|
219
241
|
email,
|
|
220
242
|
token: auth.token,
|
|
221
243
|
project: cfg.project ?? env.JIRA_PROJECT,
|
|
244
|
+
syncProjects: resolveSyncProjects(cfg, env),
|
|
245
|
+
syncMine: resolveSyncMine(cfg, env),
|
|
222
246
|
configDir: configDir(),
|
|
223
247
|
});
|
|
224
248
|
}
|
package/src/daemon/poller.ts
CHANGED
|
@@ -23,7 +23,7 @@ export async function syncOnce(
|
|
|
23
23
|
const results: { backend: string; synced: number; error?: string }[] = [];
|
|
24
24
|
for (const backend of backends) {
|
|
25
25
|
try {
|
|
26
|
-
const issues = await service.
|
|
26
|
+
const issues = await service.syncFetch(backend, DEFAULT_SYNC_LIMIT);
|
|
27
27
|
const synced = ledger.upsertMany(backend, issues);
|
|
28
28
|
results.push({ backend, synced });
|
|
29
29
|
logger?.debug("ledger sync ok", { backend, synced });
|
package/src/ports/repository.ts
CHANGED
|
@@ -100,3 +100,20 @@ export interface BoardFilterDiscoverable {
|
|
|
100
100
|
export function hasBoardFilterDiscovery(repo: IssueRepository): repo is IssueRepository & BoardFilterDiscoverable {
|
|
101
101
|
return typeof (repo as Partial<BoardFilterDiscoverable>).discoverBoardFilterJql === "function";
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Optional capability — lets a backend widen what the poller's own background
|
|
106
|
+
* sync pools into the local ledger beyond list()'s single default-project
|
|
107
|
+
* filter (Jira: additional named projects, plus everything assigned to the
|
|
108
|
+
* authenticated user, unioned into one JQL string). Returns undefined when
|
|
109
|
+
* nothing beyond the default scope is configured, so the poller falls back
|
|
110
|
+
* to plain list() unchanged. Jira only; GitHub/GitLab have no equivalent
|
|
111
|
+
* multi-project-plus-assignee query language to expand into.
|
|
112
|
+
*/
|
|
113
|
+
export interface SyncScopeExpandable {
|
|
114
|
+
buildSyncQuery(): string | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function hasSyncScopeExpansion(repo: IssueRepository): repo is IssueRepository & SyncScopeExpandable {
|
|
118
|
+
return typeof (repo as Partial<SyncScopeExpandable>).buildSyncQuery === "function";
|
|
119
|
+
}
|
|
@@ -138,7 +138,7 @@ const OPERATIONS: readonly OperationSpec[] = [
|
|
|
138
138
|
action: "ledger.search",
|
|
139
139
|
description: "Searches the local pooled-issue ledger (no live backend call).",
|
|
140
140
|
effect: "read",
|
|
141
|
-
properties: { query: stringProp, limit: numberProp },
|
|
141
|
+
properties: { query: stringProp, limit: numberProp, backend: stringProp },
|
|
142
142
|
required: ["query"],
|
|
143
143
|
},
|
|
144
144
|
{
|