@cyberismo/backend 0.0.3
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/LICENSE +702 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/middleware/commandManager.js +28 -0
- package/dist/middleware/commandManager.js.map +1 -0
- package/dist/public/THIRD-PARTY.txt +2927 -0
- package/dist/public/assets/index-DqBrVRB4.js +605 -0
- package/dist/public/assets/index-ImgtZ9pq.css +1 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/images/attach_file_add.svg +1 -0
- package/dist/public/images/cyberismo.png +0 -0
- package/dist/public/index.html +13 -0
- package/dist/public/vite.svg +1 -0
- package/dist/routes/cardTypes.js +49 -0
- package/dist/routes/cardTypes.js.map +1 -0
- package/dist/routes/cards.js +675 -0
- package/dist/routes/cards.js.map +1 -0
- package/dist/routes/fieldTypes.js +40 -0
- package/dist/routes/fieldTypes.js.map +1 -0
- package/dist/routes/linkTypes.js +40 -0
- package/dist/routes/linkTypes.js.map +1 -0
- package/dist/routes/templates.js +39 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/tree.js +38 -0
- package/dist/routes/tree.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +121 -0
- package/src/main.ts +7 -0
- package/src/middleware/commandManager.ts +43 -0
- package/src/routes/cardTypes.ts +54 -0
- package/src/routes/cards.ts +767 -0
- package/src/routes/fieldTypes.ts +51 -0
- package/src/routes/linkTypes.ts +48 -0
- package/src/routes/templates.ts +42 -0
- package/src/routes/tree.ts +40 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2024
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.
|
|
6
|
+
|
|
7
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
|
8
|
+
|
|
9
|
+
You should have received a copy of the GNU Affero General Public
|
|
10
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Hono } from 'hono';
|
|
14
|
+
import Processor from '@asciidoctor/core';
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardLocation,
|
|
18
|
+
MetadataContent,
|
|
19
|
+
ProjectFetchCardDetails,
|
|
20
|
+
} from '@cyberismo/data-handler/interfaces/project-interfaces';
|
|
21
|
+
import { CommandManager, evaluateMacros } from '@cyberismo/data-handler';
|
|
22
|
+
import { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
23
|
+
|
|
24
|
+
const router = new Hono();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @swagger
|
|
28
|
+
* /api/cards:
|
|
29
|
+
* get:
|
|
30
|
+
* summary: Returns a list of all cards and their children in the defined project.
|
|
31
|
+
* description: List of cards does not include the content of the cards, only basic metadata. Use the /api/cards/{key} endpoint to get the content of a specific card.
|
|
32
|
+
* responses:
|
|
33
|
+
* 200:
|
|
34
|
+
* description: Object containing the project cards. See definitions.ts/Card for the structure.
|
|
35
|
+
* 400:
|
|
36
|
+
* description: Error in reading project details.
|
|
37
|
+
* 500:
|
|
38
|
+
* description: project_path not set.
|
|
39
|
+
*/
|
|
40
|
+
router.get('/', async (c) => {
|
|
41
|
+
const commands = c.get('commands');
|
|
42
|
+
|
|
43
|
+
const projectResponse = await commands.showCmd.showProject();
|
|
44
|
+
|
|
45
|
+
const workflowsResponse = await commands.showCmd.showWorkflowsWithDetails();
|
|
46
|
+
if (!workflowsResponse) {
|
|
47
|
+
return c.text(`No workflows found from path ${c.get('projectPath')}`, 500);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cardTypesResponse = await commands.showCmd.showCardTypesWithDetails();
|
|
51
|
+
if (!cardTypesResponse) {
|
|
52
|
+
return c.text(`No card types found from path ${c.get('projectPath')}`, 500);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cardsResponse = await commands.showCmd.showProjectCards();
|
|
56
|
+
|
|
57
|
+
if (!cardsResponse) {
|
|
58
|
+
return c.text(`No cards found from path ${c.get('projectPath')}`, 500);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const response = {
|
|
62
|
+
name: (projectResponse! as any).name,
|
|
63
|
+
cards: cardsResponse,
|
|
64
|
+
workflows: workflowsResponse,
|
|
65
|
+
cardTypes: cardTypesResponse,
|
|
66
|
+
};
|
|
67
|
+
return c.json(response);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function getCardDetails(
|
|
71
|
+
commands: CommandManager,
|
|
72
|
+
key: string,
|
|
73
|
+
): Promise<any> {
|
|
74
|
+
const fetchCardDetails: ProjectFetchCardDetails = {
|
|
75
|
+
attachments: true,
|
|
76
|
+
children: false,
|
|
77
|
+
content: true,
|
|
78
|
+
contentType: 'adoc',
|
|
79
|
+
metadata: false,
|
|
80
|
+
parent: false,
|
|
81
|
+
location: CardLocation.projectOnly,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let cardDetailsResponse: Card | undefined;
|
|
85
|
+
try {
|
|
86
|
+
cardDetailsResponse = await commands.showCmd.showCardDetails(
|
|
87
|
+
fetchCardDetails,
|
|
88
|
+
key,
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
return { status: 400, message: `Card ${key} not found from project` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!cardDetailsResponse) {
|
|
95
|
+
return { status: 400, message: `Card ${key} not found from project` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let asciidocContent = '';
|
|
99
|
+
try {
|
|
100
|
+
asciidocContent = await evaluateMacros(cardDetailsResponse.content || '', {
|
|
101
|
+
mode: 'inject',
|
|
102
|
+
projectPath: commands.project.basePath || '',
|
|
103
|
+
cardKey: key,
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${asciidocContent}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const htmlContent = Processor()
|
|
110
|
+
.convert(asciidocContent, {
|
|
111
|
+
safe: 'safe',
|
|
112
|
+
attributes: {
|
|
113
|
+
imagesdir: `/api/cards/${key}/a`,
|
|
114
|
+
icons: 'font',
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
.toString();
|
|
118
|
+
|
|
119
|
+
// always parse for now
|
|
120
|
+
await commands.calculateCmd.generate();
|
|
121
|
+
|
|
122
|
+
const card = await commands.calculateCmd.runQuery('card', {
|
|
123
|
+
cardKey: key,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (card.length !== 1) {
|
|
127
|
+
throw new Error('Query failed. Check card-query syntax');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
status: 200,
|
|
132
|
+
data: {
|
|
133
|
+
...card[0],
|
|
134
|
+
rawContent: cardDetailsResponse.content || '',
|
|
135
|
+
parsedContent: htmlContent,
|
|
136
|
+
attachments: cardDetailsResponse.attachments,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @swagger
|
|
143
|
+
* /api/cards/{key}:
|
|
144
|
+
* get:
|
|
145
|
+
* summary: Returns the full content of a specific card including calculations.
|
|
146
|
+
* description: The key parameter is the unique identifier ("cardKey") of the card. The response includes the content as asciidoc(editable) and parsed html, which also has macros already injected
|
|
147
|
+
* parameters:
|
|
148
|
+
* - name: key
|
|
149
|
+
* in: path
|
|
150
|
+
* required: true
|
|
151
|
+
* description: Card key (string)
|
|
152
|
+
* responses:
|
|
153
|
+
* 200:
|
|
154
|
+
* description: Object containing card details. See lib/api/types.ts/CardResponse for the structure.
|
|
155
|
+
* 400:
|
|
156
|
+
* description: No search key or card not found with given key
|
|
157
|
+
* 500:
|
|
158
|
+
* description: project_path not set.
|
|
159
|
+
*/
|
|
160
|
+
router.get('/:key', async (c) => {
|
|
161
|
+
const key = c.req.param('key');
|
|
162
|
+
if (!key) {
|
|
163
|
+
return c.text('No search key', 400);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await getCardDetails(c.get('commands'), key);
|
|
167
|
+
if (result.status === 200) {
|
|
168
|
+
return c.json(result.data);
|
|
169
|
+
} else {
|
|
170
|
+
return c.text(
|
|
171
|
+
result.message || 'Unknown error',
|
|
172
|
+
result.status as ContentfulStatusCode,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @swagger
|
|
179
|
+
* /api/cards/{key}:
|
|
180
|
+
* patch:
|
|
181
|
+
* summary: Make changes to a card
|
|
182
|
+
* description: The key parameter is the unique identifier ("cardKey") of the card.
|
|
183
|
+
* parameters:
|
|
184
|
+
* - name: key
|
|
185
|
+
* in: path
|
|
186
|
+
* required: true
|
|
187
|
+
* description: Card key (string)
|
|
188
|
+
* - name: content
|
|
189
|
+
* in: body
|
|
190
|
+
* required: false
|
|
191
|
+
* description: New asciidoc content for the card. Must be a string.
|
|
192
|
+
* - name: metadata
|
|
193
|
+
* in: body
|
|
194
|
+
* type: object
|
|
195
|
+
* required: false
|
|
196
|
+
* description: New metadata for the card. Must be an object with key-value pairs.
|
|
197
|
+
* responses:
|
|
198
|
+
* 200:
|
|
199
|
+
* description: Object containing card details, same as GET. See definitions.ts/CardDetails for the structure.
|
|
200
|
+
* 207:
|
|
201
|
+
* description: Partial success. some updates failed, some succeeded. Returns card object with successful updates.
|
|
202
|
+
* 400:
|
|
203
|
+
* description: Error. Card not found, all updates failed etc. Error message in response body.
|
|
204
|
+
* 500:
|
|
205
|
+
* description: project_path not set.
|
|
206
|
+
*/
|
|
207
|
+
router.patch('/:key', async (c) => {
|
|
208
|
+
const commands = c.get('commands');
|
|
209
|
+
const key = c.req.param('key');
|
|
210
|
+
if (!key) {
|
|
211
|
+
return c.text('No search key', 400);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const body = await c.req.json();
|
|
215
|
+
let successes = 0;
|
|
216
|
+
const errors = [];
|
|
217
|
+
|
|
218
|
+
if (body.state) {
|
|
219
|
+
try {
|
|
220
|
+
await commands.transitionCmd.cardTransition(key, body.state);
|
|
221
|
+
successes++;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof Error) errors.push(error.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (body.content != null) {
|
|
228
|
+
try {
|
|
229
|
+
await commands.editCmd.editCardContent(key, body.content);
|
|
230
|
+
successes++;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof Error) errors.push(error.message);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (body.metadata) {
|
|
237
|
+
for (const [metadataKey, metadataValue] of Object.entries(body.metadata)) {
|
|
238
|
+
const value = metadataValue as MetadataContent;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await commands.editCmd.editCardMetadata(key, metadataKey, value);
|
|
242
|
+
successes++;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof Error) errors.push(error.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (body.parent) {
|
|
250
|
+
try {
|
|
251
|
+
await commands.moveCmd.moveCard(key, body.parent);
|
|
252
|
+
successes++;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error instanceof Error) errors.push(error.message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (body.index != null) {
|
|
258
|
+
try {
|
|
259
|
+
await commands.moveCmd.rankByIndex(key, body.index);
|
|
260
|
+
successes++;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof Error) errors.push(error.message);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (errors.length > 0) {
|
|
267
|
+
return c.text(errors.join('\n'), 400);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const result = await getCardDetails(commands, key);
|
|
271
|
+
if (result.status === 200) {
|
|
272
|
+
return c.json(result.data);
|
|
273
|
+
} else {
|
|
274
|
+
return c.text(
|
|
275
|
+
result.message || 'Unknown error',
|
|
276
|
+
result.status as ContentfulStatusCode,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @swagger
|
|
283
|
+
* /api/cards/{key}:
|
|
284
|
+
* delete:
|
|
285
|
+
* summary: Delete a card
|
|
286
|
+
* description: The key parameter is the unique identifier ("cardKey") of the card.
|
|
287
|
+
* parameters:
|
|
288
|
+
* - name: key
|
|
289
|
+
* in: path
|
|
290
|
+
* required: true
|
|
291
|
+
* description: Card key (string)
|
|
292
|
+
*
|
|
293
|
+
* responses:
|
|
294
|
+
* 204:
|
|
295
|
+
* description: Card deleted successfully.
|
|
296
|
+
* 400:
|
|
297
|
+
* description: Error. Card not found. Error message in response body.
|
|
298
|
+
* 500:
|
|
299
|
+
* description: project_path not set.
|
|
300
|
+
*/
|
|
301
|
+
router.delete('/:key', async (c) => {
|
|
302
|
+
const commands = c.get('commands');
|
|
303
|
+
const key = c.req.param('key');
|
|
304
|
+
if (!key) {
|
|
305
|
+
return c.text('No search key', 400);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
await commands.removeCmd.remove('card', key);
|
|
310
|
+
return new Response(null, { status: 204 });
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return c.text(
|
|
313
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
314
|
+
400,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @swagger
|
|
321
|
+
* /api/cards/{key}:
|
|
322
|
+
* post:
|
|
323
|
+
* summary: Create a new card
|
|
324
|
+
* description: Creates a new card using the specified template. If key is 'root', creates at root level.
|
|
325
|
+
* parameters:
|
|
326
|
+
* - name: key
|
|
327
|
+
* in: path
|
|
328
|
+
* required: true
|
|
329
|
+
* description: Card key (string)
|
|
330
|
+
* - name: template
|
|
331
|
+
* in: body
|
|
332
|
+
* required: true
|
|
333
|
+
* description: Template to use for creating the card
|
|
334
|
+
* responses:
|
|
335
|
+
* 200:
|
|
336
|
+
* description: Card created successfully
|
|
337
|
+
* 400:
|
|
338
|
+
* description: Error creating card or missing template
|
|
339
|
+
* 500:
|
|
340
|
+
* description: project_path not set
|
|
341
|
+
*/
|
|
342
|
+
router.post('/:key', async (c) => {
|
|
343
|
+
const key = c.req.param('key');
|
|
344
|
+
if (!key) {
|
|
345
|
+
return c.text('No search key', 400);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const body = await c.req.json();
|
|
349
|
+
if (!body.template) {
|
|
350
|
+
return c.text('template is required', 400);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const result = await c
|
|
355
|
+
.get('commands')
|
|
356
|
+
.createCmd.createCard(body.template, key === 'root' ? undefined : key);
|
|
357
|
+
|
|
358
|
+
if (result.length === 0) {
|
|
359
|
+
return c.json({ error: 'No cards created' }, 400);
|
|
360
|
+
}
|
|
361
|
+
return c.json(result);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof Error) {
|
|
364
|
+
return c.text(error.message, 400);
|
|
365
|
+
}
|
|
366
|
+
return c.text('Unknown error occurred', 500);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @swagger
|
|
372
|
+
* /api/cards/{key}/attachments:
|
|
373
|
+
* post:
|
|
374
|
+
* summary: Upload attachments to a card
|
|
375
|
+
* parameters:
|
|
376
|
+
* - name: key
|
|
377
|
+
* in: path
|
|
378
|
+
* required: true
|
|
379
|
+
* schema:
|
|
380
|
+
* type: string
|
|
381
|
+
* requestBody:
|
|
382
|
+
* content:
|
|
383
|
+
* multipart/form-data:
|
|
384
|
+
* schema:
|
|
385
|
+
* type: object
|
|
386
|
+
* properties:
|
|
387
|
+
* file:
|
|
388
|
+
* type: array
|
|
389
|
+
* items:
|
|
390
|
+
* type: string
|
|
391
|
+
* format: binary
|
|
392
|
+
* responses:
|
|
393
|
+
* 200:
|
|
394
|
+
* description: Attachments uploaded successfully
|
|
395
|
+
* 400:
|
|
396
|
+
* description: Invalid request
|
|
397
|
+
* 500:
|
|
398
|
+
* description: Server error
|
|
399
|
+
*/
|
|
400
|
+
router.post('/:key/attachments', async (c) => {
|
|
401
|
+
const commands = c.get('commands');
|
|
402
|
+
const key = c.req.param('key');
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const formData = await c.req.formData();
|
|
406
|
+
const files = formData.getAll('files');
|
|
407
|
+
if (!files || files.length === 0) {
|
|
408
|
+
return c.json({ error: 'No files uploaded' }, 400);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const succeeded = [];
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
if (file instanceof File) {
|
|
414
|
+
const buffer = await file.arrayBuffer();
|
|
415
|
+
await commands.createCmd.createAttachment(
|
|
416
|
+
key,
|
|
417
|
+
file.name,
|
|
418
|
+
Buffer.from(buffer),
|
|
419
|
+
);
|
|
420
|
+
succeeded.push(file.name);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return c.json({
|
|
425
|
+
message: 'Attachments uploaded successfully',
|
|
426
|
+
files: succeeded,
|
|
427
|
+
});
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return c.json(
|
|
430
|
+
{
|
|
431
|
+
error:
|
|
432
|
+
error instanceof Error
|
|
433
|
+
? error.message
|
|
434
|
+
: 'Failed to upload attachments',
|
|
435
|
+
},
|
|
436
|
+
500,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @swagger
|
|
443
|
+
* /api/cards/{key}/attachments/{filename}:
|
|
444
|
+
* delete:
|
|
445
|
+
* summary: Remove an attachment from a card
|
|
446
|
+
* parameters:
|
|
447
|
+
* - name: key
|
|
448
|
+
* in: path
|
|
449
|
+
* required: true
|
|
450
|
+
* schema:
|
|
451
|
+
* type: string
|
|
452
|
+
* - name: filename
|
|
453
|
+
* in: path
|
|
454
|
+
* required: true
|
|
455
|
+
* schema:
|
|
456
|
+
* type: string
|
|
457
|
+
* responses:
|
|
458
|
+
* 200:
|
|
459
|
+
* description: Attachment removed successfully
|
|
460
|
+
* 400:
|
|
461
|
+
* description: Invalid request
|
|
462
|
+
* 500:
|
|
463
|
+
* description: Server error
|
|
464
|
+
*/
|
|
465
|
+
router.delete('/:key/attachments/:filename', async (c) => {
|
|
466
|
+
const commands = c.get('commands');
|
|
467
|
+
const { key, filename } = c.req.param();
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
await commands.removeCmd.remove('attachment', key, filename);
|
|
471
|
+
return c.json({ message: 'Attachment removed successfully' });
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return c.json(
|
|
474
|
+
{
|
|
475
|
+
error:
|
|
476
|
+
error instanceof Error
|
|
477
|
+
? error.message
|
|
478
|
+
: 'Failed to remove attachment',
|
|
479
|
+
},
|
|
480
|
+
500,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @swagger
|
|
487
|
+
* /api/cards/{key}/attachments/{filename}/open:
|
|
488
|
+
* post:
|
|
489
|
+
* summary: Open an attachment using the system's default application
|
|
490
|
+
* parameters:
|
|
491
|
+
* - name: key
|
|
492
|
+
* in: path
|
|
493
|
+
* required: true
|
|
494
|
+
* schema:
|
|
495
|
+
* type: string
|
|
496
|
+
* - name: filename
|
|
497
|
+
* in: path
|
|
498
|
+
* required: true
|
|
499
|
+
* schema:
|
|
500
|
+
* type: string
|
|
501
|
+
* responses:
|
|
502
|
+
* 200:
|
|
503
|
+
* description: Attachment opened successfully
|
|
504
|
+
* 400:
|
|
505
|
+
* description: Invalid request
|
|
506
|
+
* 500:
|
|
507
|
+
* description: Server error
|
|
508
|
+
*/
|
|
509
|
+
router.post('/:key/attachments/:filename/open', async (c) => {
|
|
510
|
+
const commands = c.get('commands');
|
|
511
|
+
const { key, filename } = c.req.param();
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await commands.showCmd.openAttachment(key, filename);
|
|
515
|
+
return c.json({ message: 'Attachment opened successfully' });
|
|
516
|
+
} catch (error) {
|
|
517
|
+
return c.json(
|
|
518
|
+
{
|
|
519
|
+
error:
|
|
520
|
+
error instanceof Error ? error.message : 'Failed to open attachment',
|
|
521
|
+
},
|
|
522
|
+
500,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* @swagger
|
|
529
|
+
* /api/cards/{key}/parse:
|
|
530
|
+
* post:
|
|
531
|
+
* summary: Parse card content
|
|
532
|
+
* parameters:
|
|
533
|
+
* - name: key
|
|
534
|
+
* in: path
|
|
535
|
+
* required: true
|
|
536
|
+
* schema:
|
|
537
|
+
* type: string
|
|
538
|
+
* requestBody:
|
|
539
|
+
* content:
|
|
540
|
+
* application/json:
|
|
541
|
+
* schema:
|
|
542
|
+
* type: object
|
|
543
|
+
* properties:
|
|
544
|
+
* content:
|
|
545
|
+
* type: string
|
|
546
|
+
* responses:
|
|
547
|
+
* 200:
|
|
548
|
+
* description: Content parsed successfully
|
|
549
|
+
* 400:
|
|
550
|
+
* description: Invalid request
|
|
551
|
+
* 500:
|
|
552
|
+
* description: Server error
|
|
553
|
+
*/
|
|
554
|
+
router.post('/:key/parse', async (c) => {
|
|
555
|
+
const commands = c.get('commands');
|
|
556
|
+
const key = c.req.param('key');
|
|
557
|
+
const { content } = await c.req.json();
|
|
558
|
+
|
|
559
|
+
if (content == null) {
|
|
560
|
+
return c.json({ error: 'Content is required' }, 400);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
let asciidocContent = '';
|
|
565
|
+
try {
|
|
566
|
+
asciidocContent = await evaluateMacros(content, {
|
|
567
|
+
mode: 'inject',
|
|
568
|
+
projectPath: commands.project.basePath || '',
|
|
569
|
+
cardKey: key,
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const processor = Processor();
|
|
576
|
+
const parsedContent = processor
|
|
577
|
+
.convert(asciidocContent, {
|
|
578
|
+
safe: 'safe',
|
|
579
|
+
attributes: {
|
|
580
|
+
imagesdir: `/api/cards/${key}/a`,
|
|
581
|
+
icons: 'font',
|
|
582
|
+
},
|
|
583
|
+
})
|
|
584
|
+
.toString();
|
|
585
|
+
|
|
586
|
+
return c.json({ parsedContent });
|
|
587
|
+
} catch (error) {
|
|
588
|
+
return c.json(
|
|
589
|
+
{
|
|
590
|
+
error:
|
|
591
|
+
error instanceof Error ? error.message : 'Failed to parse content',
|
|
592
|
+
},
|
|
593
|
+
500,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @swagger
|
|
600
|
+
* /api/cards/{key}/links:
|
|
601
|
+
* post:
|
|
602
|
+
* summary: Create a link between cards
|
|
603
|
+
* parameters:
|
|
604
|
+
* - name: key
|
|
605
|
+
* in: path
|
|
606
|
+
* required: true
|
|
607
|
+
* schema:
|
|
608
|
+
* type: string
|
|
609
|
+
* requestBody:
|
|
610
|
+
* content:
|
|
611
|
+
* application/json:
|
|
612
|
+
* schema:
|
|
613
|
+
* type: object
|
|
614
|
+
* properties:
|
|
615
|
+
* toCard:
|
|
616
|
+
* type: string
|
|
617
|
+
* linkType:
|
|
618
|
+
* type: string
|
|
619
|
+
* description:
|
|
620
|
+
* type: string
|
|
621
|
+
* responses:
|
|
622
|
+
* 200:
|
|
623
|
+
* description: Link created successfully
|
|
624
|
+
* 400:
|
|
625
|
+
* description: Invalid request
|
|
626
|
+
* 500:
|
|
627
|
+
* description: Server error
|
|
628
|
+
*/
|
|
629
|
+
router.post('/:key/links', async (c) => {
|
|
630
|
+
const commands = c.get('commands');
|
|
631
|
+
const key = c.req.param('key');
|
|
632
|
+
const { toCard, linkType, description } = await c.req.json();
|
|
633
|
+
|
|
634
|
+
if (!toCard || !linkType) {
|
|
635
|
+
return c.json({ error: 'toCard and linkType are required' }, 400);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
await commands.createCmd.createLink(key, toCard, linkType, description);
|
|
640
|
+
return c.json({ message: 'Link created successfully' });
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return c.json(
|
|
643
|
+
{
|
|
644
|
+
error: error instanceof Error ? error.message : 'Failed to create link',
|
|
645
|
+
},
|
|
646
|
+
500,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* @swagger
|
|
653
|
+
* /api/cards/{key}/links:
|
|
654
|
+
* delete:
|
|
655
|
+
* summary: Remove a link between cards
|
|
656
|
+
* parameters:
|
|
657
|
+
* - name: key
|
|
658
|
+
* in: path
|
|
659
|
+
* required: true
|
|
660
|
+
* schema:
|
|
661
|
+
* type: string
|
|
662
|
+
* requestBody:
|
|
663
|
+
* content:
|
|
664
|
+
* application/json:
|
|
665
|
+
* schema:
|
|
666
|
+
* type: object
|
|
667
|
+
* properties:
|
|
668
|
+
* toCard:
|
|
669
|
+
* type: string
|
|
670
|
+
* linkType:
|
|
671
|
+
* type: string
|
|
672
|
+
* description:
|
|
673
|
+
* type: string
|
|
674
|
+
* responses:
|
|
675
|
+
* 200:
|
|
676
|
+
* description: Link removed successfully
|
|
677
|
+
* 400:
|
|
678
|
+
* description: Invalid request
|
|
679
|
+
* 500:
|
|
680
|
+
* description: Server error
|
|
681
|
+
*/
|
|
682
|
+
router.delete('/:key/links', async (c) => {
|
|
683
|
+
const commands = c.get('commands');
|
|
684
|
+
const key = c.req.param('key');
|
|
685
|
+
const { toCard, linkType, description } = await c.req.json();
|
|
686
|
+
|
|
687
|
+
if (!toCard || !linkType) {
|
|
688
|
+
return c.json({ error: 'toCard and linkType are required' }, 400);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
await commands.removeCmd.remove('link', key, toCard, linkType, description);
|
|
693
|
+
return c.json({ message: 'Link removed successfully' });
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return c.json(
|
|
696
|
+
{
|
|
697
|
+
error: error instanceof Error ? error.message : 'Failed to remove link',
|
|
698
|
+
},
|
|
699
|
+
500,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* @swagger
|
|
706
|
+
* /api/cards/{key}/a/{attachment}:
|
|
707
|
+
* get:
|
|
708
|
+
* summary: Returns an attachment file for a specific card.
|
|
709
|
+
* parameters:
|
|
710
|
+
* - name: key
|
|
711
|
+
* in: path
|
|
712
|
+
* required: true
|
|
713
|
+
* description: Card key (string)
|
|
714
|
+
* - name: attachment
|
|
715
|
+
* in: path
|
|
716
|
+
* required: true
|
|
717
|
+
* description: file name of the attachment
|
|
718
|
+
* responses:
|
|
719
|
+
* 200:
|
|
720
|
+
* description: Attachment object as a file buffer, content-type set to the mime type of the file
|
|
721
|
+
* 400:
|
|
722
|
+
* description: No search key or card not found with given key
|
|
723
|
+
* 404:
|
|
724
|
+
* description: Attachment file not found
|
|
725
|
+
* 500:
|
|
726
|
+
* description: project_path not set.
|
|
727
|
+
*/
|
|
728
|
+
router.get('/:key/a/:attachment', async (c) => {
|
|
729
|
+
const commands = c.get('commands');
|
|
730
|
+
const { key, attachment } = c.req.param();
|
|
731
|
+
const filename = decodeURI(attachment);
|
|
732
|
+
|
|
733
|
+
if (!filename || !key) {
|
|
734
|
+
return c.text('Missing cardKey or filename', 400);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const attachmentResponse = await commands.showCmd.showAttachment(
|
|
739
|
+
key,
|
|
740
|
+
filename,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
if (!attachmentResponse) {
|
|
744
|
+
return c.text(
|
|
745
|
+
`No attachment found from card ${key} and filename ${filename}`,
|
|
746
|
+
404,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const payload = attachmentResponse as any;
|
|
751
|
+
|
|
752
|
+
return new Response(payload.fileBuffer, {
|
|
753
|
+
headers: {
|
|
754
|
+
'Content-Type': payload.mimeType,
|
|
755
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
756
|
+
'Cache-Control': 'no-store',
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
} catch {
|
|
760
|
+
return c.text(
|
|
761
|
+
`No attachment found from card ${key} and filename ${filename}`,
|
|
762
|
+
404,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
export default router;
|