@martin4455/redmine-mcp-ro 0.3.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/CONFIG.md +242 -0
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/api-key-filter.d.ts +6 -0
- package/dist/api-key-filter.js +16 -0
- package/dist/attachment-downloader.d.ts +15 -0
- package/dist/attachment-downloader.js +190 -0
- package/dist/attachment-parser.d.ts +27 -0
- package/dist/attachment-parser.js +138 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +144 -0
- package/dist/config-file.d.ts +18 -0
- package/dist/config-file.js +192 -0
- package/dist/config-loader.d.ts +14 -0
- package/dist/config-loader.js +58 -0
- package/dist/credential-store.d.ts +17 -0
- package/dist/credential-store.js +71 -0
- package/dist/download-directory.d.ts +3 -0
- package/dist/download-directory.js +51 -0
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.js +99 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/local-configuration.d.ts +35 -0
- package/dist/local-configuration.js +218 -0
- package/dist/redmine-client.d.ts +100 -0
- package/dist/redmine-client.js +184 -0
- package/dist/safe-error.d.ts +2 -0
- package/dist/safe-error.js +18 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +222 -0
- package/dist/tool-definitions.d.ts +2 -0
- package/dist/tool-definitions.js +460 -0
- package/dist/tool-validation.d.ts +7 -0
- package/dist/tool-validation.js +20 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.js +1 -0
- package/dist/user-resolver.d.ts +24 -0
- package/dist/user-resolver.js +140 -0
- package/package.json +78 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ConfigurationError, localConfigPath, normalizeRedmineUrl, readConfig, requireModern, validateProfileName, withConfigLock, writeConfig, } from './config-file.js';
|
|
3
|
+
import { SystemCredentialStore } from './credential-store.js';
|
|
4
|
+
function migratedProfileNames(names) {
|
|
5
|
+
const result = new Map();
|
|
6
|
+
// Reserve valid names first, independent of their order in the old file.
|
|
7
|
+
for (const name of names) {
|
|
8
|
+
try {
|
|
9
|
+
validateProfileName(name);
|
|
10
|
+
result.set(name, name);
|
|
11
|
+
}
|
|
12
|
+
catch { /* Rename legacy names below. */ }
|
|
13
|
+
}
|
|
14
|
+
const used = new Set(result.values());
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
if (result.has(name))
|
|
17
|
+
continue;
|
|
18
|
+
let base = name.normalize('NFKD').replace(/\p{M}/gu, '').replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
19
|
+
.replace(/^[^a-zA-Z0-9]+/, '').slice(0, 64);
|
|
20
|
+
if (!base || ['constructor', 'prototype'].includes(base))
|
|
21
|
+
base = 'profile';
|
|
22
|
+
let target = base;
|
|
23
|
+
for (let suffix = 2; used.has(target); suffix++) {
|
|
24
|
+
const ending = `-${suffix}`;
|
|
25
|
+
target = base.slice(0, 64 - ending.length) + ending;
|
|
26
|
+
}
|
|
27
|
+
validateProfileName(target);
|
|
28
|
+
result.set(name, target);
|
|
29
|
+
used.add(target);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
async function storeOperation(operation) {
|
|
34
|
+
try {
|
|
35
|
+
return await operation();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof ConfigurationError)
|
|
39
|
+
throw error;
|
|
40
|
+
throw new ConfigurationError('System credential operation failed. No plaintext fallback is used.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function validateApiKey(value) {
|
|
44
|
+
if (!value || value !== value.trim() || /[\r\n\0]/.test(value)) {
|
|
45
|
+
throw new ConfigurationError('API key must be nonempty and contain no surrounding whitespace or control line breaks.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function parseConnection(raw) {
|
|
49
|
+
if (raw === null)
|
|
50
|
+
throw new ConfigurationError('Credential not found in the system store. Configure this profile on this computer.');
|
|
51
|
+
try {
|
|
52
|
+
const payload = JSON.parse(raw);
|
|
53
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) ||
|
|
54
|
+
Object.keys(payload).some(key => !['version', 'url', 'apiKey'].includes(key)) ||
|
|
55
|
+
payload.version !== 1 || typeof payload.url !== 'string' || typeof payload.apiKey !== 'string')
|
|
56
|
+
throw new Error();
|
|
57
|
+
validateApiKey(payload.apiKey);
|
|
58
|
+
return { url: normalizeRedmineUrl(payload.url), apiKey: payload.apiKey };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new ConfigurationError('Invalid credential in the system store. Reconfigure this profile.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function readConnection(store, profile) {
|
|
65
|
+
return parseConnection(await storeOperation(() => store.get(profile.credentialId)));
|
|
66
|
+
}
|
|
67
|
+
export class LocalConfiguration {
|
|
68
|
+
configPath;
|
|
69
|
+
store;
|
|
70
|
+
legacyStore;
|
|
71
|
+
constructor(options = {}) {
|
|
72
|
+
this.configPath = localConfigPath(options.directory);
|
|
73
|
+
this.store = options.store ?? new SystemCredentialStore();
|
|
74
|
+
// Tests with an injected store never access the user's real keyring.
|
|
75
|
+
this.legacyStore = options.legacyStore ?? (options.store ? undefined : new SystemCredentialStore('legacy'));
|
|
76
|
+
}
|
|
77
|
+
read() {
|
|
78
|
+
const { config } = readConfig(this.configPath);
|
|
79
|
+
return config ? requireModern(config) : null;
|
|
80
|
+
}
|
|
81
|
+
async cleanup(ids) {
|
|
82
|
+
const warnings = [];
|
|
83
|
+
for (const id of ids) {
|
|
84
|
+
try {
|
|
85
|
+
await storeOperation(() => this.store.delete(id));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
warnings.push(`Could not delete credential ${id} from the system store. Remove that entry manually from service @martin4455/redmine-mcp-ro.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return warnings;
|
|
92
|
+
}
|
|
93
|
+
async createCredential(url, apiKey) {
|
|
94
|
+
validateApiKey(apiKey);
|
|
95
|
+
const connection = { version: 1, url: normalizeRedmineUrl(url), apiKey };
|
|
96
|
+
const profile = { credentialId: randomUUID() };
|
|
97
|
+
const payload = JSON.stringify(connection);
|
|
98
|
+
try {
|
|
99
|
+
await storeOperation(() => this.store.set(profile.credentialId, payload));
|
|
100
|
+
const stored = await storeOperation(() => this.store.get(profile.credentialId));
|
|
101
|
+
if (stored === null)
|
|
102
|
+
throw new ConfigurationError('Credential not found after saving to the system store.');
|
|
103
|
+
if (stored !== payload) {
|
|
104
|
+
throw new ConfigurationError('Credential verification failed. Configuration was not saved.');
|
|
105
|
+
}
|
|
106
|
+
return profile;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const warnings = await this.cleanup([profile.credentialId]);
|
|
110
|
+
if (warnings.length)
|
|
111
|
+
throw new ConfigurationError(`${error instanceof ConfigurationError ? error.message : 'Credential operation failed.'} ${warnings.join(' ')}`);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async configure(name, url, apiKey) {
|
|
116
|
+
validateProfileName(name);
|
|
117
|
+
normalizeRedmineUrl(url);
|
|
118
|
+
validateApiKey(apiKey);
|
|
119
|
+
return withConfigLock(this.configPath, async () => {
|
|
120
|
+
const snapshot = readConfig(this.configPath);
|
|
121
|
+
const config = requireModern(snapshot.config);
|
|
122
|
+
const previous = config.profiles[name];
|
|
123
|
+
const profile = await this.createCredential(url, apiKey);
|
|
124
|
+
config.profiles[name] = profile;
|
|
125
|
+
config.defaultProfile ??= name;
|
|
126
|
+
try {
|
|
127
|
+
writeConfig(this.configPath, config, snapshot.raw);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const warnings = await this.cleanup([profile.credentialId]);
|
|
131
|
+
if (warnings.length)
|
|
132
|
+
throw new ConfigurationError(`Configuration was not saved. ${warnings.join(' ')}`);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
return { config, warnings: await this.cleanup(previous ? [previous.credentialId] : []) };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async migrate() {
|
|
139
|
+
return withConfigLock(this.configPath, async () => {
|
|
140
|
+
const snapshot = readConfig(this.configPath);
|
|
141
|
+
if (!snapshot.config)
|
|
142
|
+
throw new ConfigurationError('No local configuration to migrate.');
|
|
143
|
+
if (snapshot.config.version === '3.0')
|
|
144
|
+
return { config: snapshot.config, warnings: [] };
|
|
145
|
+
const previous = snapshot.config;
|
|
146
|
+
const names = migratedProfileNames(Object.keys(previous.profiles));
|
|
147
|
+
const config = { version: '3.0', profiles: Object.create(null),
|
|
148
|
+
...(previous.settings ? { settings: { ...previous.settings } } : {}) };
|
|
149
|
+
const created = [];
|
|
150
|
+
try {
|
|
151
|
+
if (previous.version === '1.0') {
|
|
152
|
+
for (const [name, profile] of Object.entries(previous.profiles)) {
|
|
153
|
+
const stored = await this.createCredential(profile.url, profile.apiKey);
|
|
154
|
+
created.push(stored.credentialId);
|
|
155
|
+
config.profiles[names.get(name)] = stored;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
for (const [name, profile] of Object.entries(previous.profiles)) {
|
|
160
|
+
let raw = await storeOperation(() => this.store.get(profile.credentialId));
|
|
161
|
+
const inCurrentStore = raw !== null;
|
|
162
|
+
if (raw === null && this.legacyStore) {
|
|
163
|
+
raw = await storeOperation(() => this.legacyStore.get(profile.credentialId));
|
|
164
|
+
}
|
|
165
|
+
const connection = parseConnection(raw);
|
|
166
|
+
if (profile.url !== undefined && normalizeRedmineUrl(profile.url) !== connection.url) {
|
|
167
|
+
throw new ConfigurationError('The legacy URL does not match the connection in the system store. Configuration was not changed.');
|
|
168
|
+
}
|
|
169
|
+
if (inCurrentStore)
|
|
170
|
+
config.profiles[names.get(name)] = { credentialId: profile.credentialId };
|
|
171
|
+
else {
|
|
172
|
+
const stored = await this.createCredential(connection.url, connection.apiKey);
|
|
173
|
+
created.push(stored.credentialId);
|
|
174
|
+
config.profiles[names.get(name)] = stored;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
config.defaultProfile = (previous.defaultProfile === undefined ? undefined : names.get(previous.defaultProfile)) ??
|
|
179
|
+
(Object.hasOwn(config.profiles, 'default') ? 'default' : Object.keys(config.profiles)[0]);
|
|
180
|
+
writeConfig(this.configPath, config, snapshot.raw);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const warnings = await this.cleanup(created);
|
|
184
|
+
if (warnings.length)
|
|
185
|
+
throw new ConfigurationError(`Migration did not complete; the original file was not replaced. ${warnings.join(' ')}`);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
// Historical-service entries are retained: another local copy may still use them.
|
|
189
|
+
return { config, warnings: [], profileRenames: [...names].filter(([from, to]) => from !== to).map(([from, to]) => ({ from, to })) };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async useProfile(name) {
|
|
193
|
+
validateProfileName(name);
|
|
194
|
+
await withConfigLock(this.configPath, async () => {
|
|
195
|
+
const snapshot = readConfig(this.configPath);
|
|
196
|
+
const config = requireModern(snapshot.config);
|
|
197
|
+
if (!Object.hasOwn(config.profiles, name))
|
|
198
|
+
throw new ConfigurationError('Profile not found in this local configuration.');
|
|
199
|
+
config.defaultProfile = name;
|
|
200
|
+
writeConfig(this.configPath, config, snapshot.raw);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async remove(name) {
|
|
204
|
+
validateProfileName(name);
|
|
205
|
+
return withConfigLock(this.configPath, async () => {
|
|
206
|
+
const snapshot = readConfig(this.configPath);
|
|
207
|
+
const config = requireModern(snapshot.config);
|
|
208
|
+
if (!Object.hasOwn(config.profiles, name))
|
|
209
|
+
throw new ConfigurationError('Profile not found in this local configuration.');
|
|
210
|
+
const id = config.profiles[name].credentialId;
|
|
211
|
+
delete config.profiles[name];
|
|
212
|
+
if (config.defaultProfile === name)
|
|
213
|
+
config.defaultProfile = Object.keys(config.profiles)[0];
|
|
214
|
+
writeConfig(this.configPath, config, snapshot.raw);
|
|
215
|
+
return { config, warnings: await this.cleanup([id]) };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { RedmineConfig, RedmineIssue, RedmineProject, RedmineUser, RedmineTimeEntry, RedmineVersion, RedmineNews, RedmineWikiPage, IssueFilterParams, PaginationParams } from './types.js';
|
|
2
|
+
export declare class RedmineClient {
|
|
3
|
+
private client;
|
|
4
|
+
constructor(config: RedmineConfig);
|
|
5
|
+
getIssues(params?: IssueFilterParams): Promise<{
|
|
6
|
+
issues: RedmineIssue[];
|
|
7
|
+
total_count: number;
|
|
8
|
+
}>;
|
|
9
|
+
getIssue(id: number, include?: string[]): Promise<RedmineIssue>;
|
|
10
|
+
getProjects(params?: PaginationParams): Promise<{
|
|
11
|
+
projects: RedmineProject[];
|
|
12
|
+
total_count: number;
|
|
13
|
+
}>;
|
|
14
|
+
getProject(id: string | number, include?: string[]): Promise<RedmineProject>;
|
|
15
|
+
getUsers(params?: PaginationParams): Promise<{
|
|
16
|
+
users: RedmineUser[];
|
|
17
|
+
total_count: number;
|
|
18
|
+
}>;
|
|
19
|
+
getUser(id: number, include?: string[]): Promise<RedmineUser>;
|
|
20
|
+
getCurrentUser(): Promise<RedmineUser>;
|
|
21
|
+
getTimeEntries(params?: PaginationParams & {
|
|
22
|
+
project_id?: number;
|
|
23
|
+
user_id?: number;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
time_entries: RedmineTimeEntry[];
|
|
26
|
+
total_count: number;
|
|
27
|
+
}>;
|
|
28
|
+
getTimeEntry(id: number): Promise<RedmineTimeEntry>;
|
|
29
|
+
getProjectVersions(projectId: string | number): Promise<{
|
|
30
|
+
versions: RedmineVersion[];
|
|
31
|
+
}>;
|
|
32
|
+
getVersion(id: number): Promise<RedmineVersion>;
|
|
33
|
+
getNews(params?: PaginationParams): Promise<{
|
|
34
|
+
news: RedmineNews[];
|
|
35
|
+
}>;
|
|
36
|
+
getProjectNews(projectId: string | number, params?: PaginationParams): Promise<{
|
|
37
|
+
news: RedmineNews[];
|
|
38
|
+
}>;
|
|
39
|
+
getWikiPages(projectId: string | number): Promise<{
|
|
40
|
+
wiki_pages: Array<{
|
|
41
|
+
title: string;
|
|
42
|
+
version: number;
|
|
43
|
+
created_on: string;
|
|
44
|
+
updated_on: string;
|
|
45
|
+
}>;
|
|
46
|
+
}>;
|
|
47
|
+
getWikiPage(projectId: string | number, title: string, version?: number): Promise<{
|
|
48
|
+
wiki_page: RedmineWikiPage;
|
|
49
|
+
}>;
|
|
50
|
+
getProjectMemberships(projectId: string | number, params?: PaginationParams): Promise<any>;
|
|
51
|
+
getMembership(id: number): Promise<any>;
|
|
52
|
+
getIssueStatuses(): Promise<{
|
|
53
|
+
issue_statuses: any[];
|
|
54
|
+
}>;
|
|
55
|
+
getTrackers(): Promise<{
|
|
56
|
+
trackers: any[];
|
|
57
|
+
}>;
|
|
58
|
+
getIssuePriorities(): Promise<{
|
|
59
|
+
issue_priorities: any[];
|
|
60
|
+
}>;
|
|
61
|
+
getTimeEntryActivities(): Promise<{
|
|
62
|
+
time_entry_activities: any[];
|
|
63
|
+
}>;
|
|
64
|
+
getRoles(): Promise<{
|
|
65
|
+
roles: any[];
|
|
66
|
+
}>;
|
|
67
|
+
getRole(id: number): Promise<any>;
|
|
68
|
+
getGroups(params?: PaginationParams): Promise<{
|
|
69
|
+
groups: any[];
|
|
70
|
+
total_count: number;
|
|
71
|
+
}>;
|
|
72
|
+
getGroup(id: number, include?: string[]): Promise<any>;
|
|
73
|
+
getCustomFields(): Promise<{
|
|
74
|
+
custom_fields: any[];
|
|
75
|
+
}>;
|
|
76
|
+
search(query: string, params?: {
|
|
77
|
+
issues?: boolean;
|
|
78
|
+
news?: boolean;
|
|
79
|
+
documents?: boolean;
|
|
80
|
+
changesets?: boolean;
|
|
81
|
+
wiki_pages?: boolean;
|
|
82
|
+
messages?: boolean;
|
|
83
|
+
projects?: boolean;
|
|
84
|
+
}): Promise<any>;
|
|
85
|
+
getProjectIssueCategories(projectId: string | number): Promise<{
|
|
86
|
+
issue_categories: any[];
|
|
87
|
+
}>;
|
|
88
|
+
getIssueCategory(id: number): Promise<any>;
|
|
89
|
+
getAttachment(id: number): Promise<any>;
|
|
90
|
+
getIssueRelations(issueId: number): Promise<{
|
|
91
|
+
relations: any[];
|
|
92
|
+
}>;
|
|
93
|
+
getRelation(id: number): Promise<any>;
|
|
94
|
+
getQueries(params?: PaginationParams): Promise<{
|
|
95
|
+
queries: any[];
|
|
96
|
+
}>;
|
|
97
|
+
getProjectFiles(projectId: string | number): Promise<{
|
|
98
|
+
files: any[];
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createRedmineHttpClient, positiveInteger } from './http-client.js';
|
|
2
|
+
import { stripApiKeys } from './api-key-filter.js';
|
|
3
|
+
function pathSegment(value) {
|
|
4
|
+
if (typeof value === 'number')
|
|
5
|
+
positiveInteger(value, 'id');
|
|
6
|
+
if (String(value) === '.' || String(value) === '..')
|
|
7
|
+
throw new Error('Invalid path segment');
|
|
8
|
+
return encodeURIComponent(String(value));
|
|
9
|
+
}
|
|
10
|
+
export class RedmineClient {
|
|
11
|
+
client;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.client = createRedmineHttpClient(config);
|
|
14
|
+
// Types alone do not remove extra fields from Redmine's JSON responses.
|
|
15
|
+
this.client.interceptors.response.use(response => {
|
|
16
|
+
response.data = stripApiKeys(response.data);
|
|
17
|
+
return response;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Issues endpoints
|
|
21
|
+
async getIssues(params) {
|
|
22
|
+
const response = await this.client.get('/issues.json', { params });
|
|
23
|
+
return response.data;
|
|
24
|
+
}
|
|
25
|
+
async getIssue(id, include) {
|
|
26
|
+
const params = include ? { include: include.join(',') } : undefined;
|
|
27
|
+
const response = await this.client.get(`/issues/${pathSegment(id)}.json`, { params });
|
|
28
|
+
return response.data.issue;
|
|
29
|
+
}
|
|
30
|
+
// Projects endpoints
|
|
31
|
+
async getProjects(params) {
|
|
32
|
+
const response = await this.client.get('/projects.json', { params });
|
|
33
|
+
return response.data;
|
|
34
|
+
}
|
|
35
|
+
async getProject(id, include) {
|
|
36
|
+
const params = include ? { include: include.join(',') } : undefined;
|
|
37
|
+
const response = await this.client.get(`/projects/${pathSegment(id)}.json`, { params });
|
|
38
|
+
return response.data.project;
|
|
39
|
+
}
|
|
40
|
+
// Users endpoints
|
|
41
|
+
async getUsers(params) {
|
|
42
|
+
const response = await this.client.get('/users.json', { params });
|
|
43
|
+
return response.data;
|
|
44
|
+
}
|
|
45
|
+
async getUser(id, include) {
|
|
46
|
+
const params = include ? { include: include.join(',') } : undefined;
|
|
47
|
+
const response = await this.client.get(`/users/${pathSegment(id)}.json`, { params });
|
|
48
|
+
return response.data.user;
|
|
49
|
+
}
|
|
50
|
+
async getCurrentUser() {
|
|
51
|
+
const response = await this.client.get('/users/current.json');
|
|
52
|
+
return response.data.user;
|
|
53
|
+
}
|
|
54
|
+
// Time entries endpoints
|
|
55
|
+
async getTimeEntries(params) {
|
|
56
|
+
const response = await this.client.get('/time_entries.json', { params });
|
|
57
|
+
return response.data;
|
|
58
|
+
}
|
|
59
|
+
async getTimeEntry(id) {
|
|
60
|
+
const response = await this.client.get(`/time_entries/${pathSegment(id)}.json`);
|
|
61
|
+
return response.data.time_entry;
|
|
62
|
+
}
|
|
63
|
+
// Versions endpoints
|
|
64
|
+
async getProjectVersions(projectId) {
|
|
65
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/versions.json`);
|
|
66
|
+
return response.data;
|
|
67
|
+
}
|
|
68
|
+
async getVersion(id) {
|
|
69
|
+
const response = await this.client.get(`/versions/${pathSegment(id)}.json`);
|
|
70
|
+
return response.data.version;
|
|
71
|
+
}
|
|
72
|
+
// News endpoints
|
|
73
|
+
async getNews(params) {
|
|
74
|
+
const response = await this.client.get('/news.json', { params });
|
|
75
|
+
return response.data;
|
|
76
|
+
}
|
|
77
|
+
async getProjectNews(projectId, params) {
|
|
78
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/news.json`, { params });
|
|
79
|
+
return response.data;
|
|
80
|
+
}
|
|
81
|
+
// Wiki endpoints
|
|
82
|
+
async getWikiPages(projectId) {
|
|
83
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/wiki/index.json`);
|
|
84
|
+
return response.data;
|
|
85
|
+
}
|
|
86
|
+
async getWikiPage(projectId, title, version) {
|
|
87
|
+
const url = version
|
|
88
|
+
? `/projects/${pathSegment(projectId)}/wiki/${pathSegment(title)}/${pathSegment(version)}.json`
|
|
89
|
+
: `/projects/${pathSegment(projectId)}/wiki/${pathSegment(title)}.json`;
|
|
90
|
+
const response = await this.client.get(url);
|
|
91
|
+
return response.data;
|
|
92
|
+
}
|
|
93
|
+
// Memberships endpoints
|
|
94
|
+
async getProjectMemberships(projectId, params) {
|
|
95
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/memberships.json`, { params });
|
|
96
|
+
return response.data;
|
|
97
|
+
}
|
|
98
|
+
async getMembership(id) {
|
|
99
|
+
const response = await this.client.get(`/memberships/${pathSegment(id)}.json`);
|
|
100
|
+
return response.data.membership;
|
|
101
|
+
}
|
|
102
|
+
// Issue statuses, trackers, priorities, etc.
|
|
103
|
+
async getIssueStatuses() {
|
|
104
|
+
const response = await this.client.get('/issue_statuses.json');
|
|
105
|
+
return response.data;
|
|
106
|
+
}
|
|
107
|
+
async getTrackers() {
|
|
108
|
+
const response = await this.client.get('/trackers.json');
|
|
109
|
+
return response.data;
|
|
110
|
+
}
|
|
111
|
+
async getIssuePriorities() {
|
|
112
|
+
const response = await this.client.get('/enumerations/issue_priorities.json');
|
|
113
|
+
return response.data;
|
|
114
|
+
}
|
|
115
|
+
async getTimeEntryActivities() {
|
|
116
|
+
const response = await this.client.get('/enumerations/time_entry_activities.json');
|
|
117
|
+
return response.data;
|
|
118
|
+
}
|
|
119
|
+
// Roles and groups
|
|
120
|
+
async getRoles() {
|
|
121
|
+
const response = await this.client.get('/roles.json');
|
|
122
|
+
return response.data;
|
|
123
|
+
}
|
|
124
|
+
async getRole(id) {
|
|
125
|
+
const response = await this.client.get(`/roles/${pathSegment(id)}.json`);
|
|
126
|
+
return response.data.role;
|
|
127
|
+
}
|
|
128
|
+
async getGroups(params) {
|
|
129
|
+
const response = await this.client.get('/groups.json', { params });
|
|
130
|
+
return response.data;
|
|
131
|
+
}
|
|
132
|
+
async getGroup(id, include) {
|
|
133
|
+
const params = include ? { include: include.join(',') } : undefined;
|
|
134
|
+
const response = await this.client.get(`/groups/${pathSegment(id)}.json`, { params });
|
|
135
|
+
return response.data.group;
|
|
136
|
+
}
|
|
137
|
+
// Custom fields
|
|
138
|
+
async getCustomFields() {
|
|
139
|
+
const response = await this.client.get('/custom_fields.json');
|
|
140
|
+
return response.data;
|
|
141
|
+
}
|
|
142
|
+
// Search
|
|
143
|
+
async search(query, params) {
|
|
144
|
+
const searchParams = {
|
|
145
|
+
q: query,
|
|
146
|
+
...params
|
|
147
|
+
};
|
|
148
|
+
const response = await this.client.get('/search.json', { params: searchParams });
|
|
149
|
+
return response.data;
|
|
150
|
+
}
|
|
151
|
+
// Issue categories
|
|
152
|
+
async getProjectIssueCategories(projectId) {
|
|
153
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/issue_categories.json`);
|
|
154
|
+
return response.data;
|
|
155
|
+
}
|
|
156
|
+
async getIssueCategory(id) {
|
|
157
|
+
const response = await this.client.get(`/issue_categories/${pathSegment(id)}.json`);
|
|
158
|
+
return response.data.issue_category;
|
|
159
|
+
}
|
|
160
|
+
// Attachments
|
|
161
|
+
async getAttachment(id) {
|
|
162
|
+
const response = await this.client.get(`/attachments/${pathSegment(id)}.json`);
|
|
163
|
+
return response.data.attachment;
|
|
164
|
+
}
|
|
165
|
+
// Issue relations
|
|
166
|
+
async getIssueRelations(issueId) {
|
|
167
|
+
const response = await this.client.get(`/issues/${pathSegment(issueId)}/relations.json`);
|
|
168
|
+
return response.data;
|
|
169
|
+
}
|
|
170
|
+
async getRelation(id) {
|
|
171
|
+
const response = await this.client.get(`/relations/${pathSegment(id)}.json`);
|
|
172
|
+
return response.data.relation;
|
|
173
|
+
}
|
|
174
|
+
// Queries
|
|
175
|
+
async getQueries(params) {
|
|
176
|
+
const response = await this.client.get('/queries.json', { params });
|
|
177
|
+
return response.data;
|
|
178
|
+
}
|
|
179
|
+
// Files
|
|
180
|
+
async getProjectFiles(projectId) {
|
|
181
|
+
const response = await this.client.get(`/projects/${pathSegment(projectId)}/files.json`);
|
|
182
|
+
return response.data;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
/** Never serialize AxiosError: its config and request contain authentication headers. */
|
|
3
|
+
export function safeErrorMessage(error, secrets = []) {
|
|
4
|
+
let message;
|
|
5
|
+
if (axios.isAxiosError(error)) {
|
|
6
|
+
const status = error.response?.status;
|
|
7
|
+
const code = error.code && /^[A-Z0-9_]+$/.test(error.code) ? error.code : undefined;
|
|
8
|
+
message = `HTTP request failed${status ? ` (status ${status})` : ''}${code ? ` [${code}]` : ''}`;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
message = error instanceof Error ? error.message : 'Unknown error';
|
|
12
|
+
}
|
|
13
|
+
for (const secret of secrets) {
|
|
14
|
+
if (secret)
|
|
15
|
+
message = message.split(secret).join('[REDACTED]');
|
|
16
|
+
}
|
|
17
|
+
return message;
|
|
18
|
+
}
|
package/dist/server.d.ts
ADDED