@cyberismo/backend 0.0.6 → 0.0.8

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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/app.js +65 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/export.js +228 -0
  5. package/dist/export.js.map +1 -0
  6. package/dist/index.js +41 -76
  7. package/dist/index.js.map +1 -1
  8. package/dist/main.js +19 -1
  9. package/dist/main.js.map +1 -1
  10. package/dist/public/THIRD-PARTY.txt +47 -47
  11. package/dist/public/assets/index-BngW8o1w.css +1 -0
  12. package/dist/public/assets/index-D5kiRHuF.js +111171 -0
  13. package/dist/public/config.json +3 -0
  14. package/dist/public/index.html +11 -2
  15. package/dist/routes/cards.js +48 -20
  16. package/dist/routes/cards.js.map +1 -1
  17. package/dist/routes/fieldTypes.js +3 -1
  18. package/dist/routes/fieldTypes.js.map +1 -1
  19. package/dist/routes/linkTypes.js +3 -1
  20. package/dist/routes/linkTypes.js.map +1 -1
  21. package/dist/routes/templates.js +8 -1
  22. package/dist/routes/templates.js.map +1 -1
  23. package/dist/routes/tree.js +3 -1
  24. package/dist/routes/tree.js.map +1 -1
  25. package/dist/utils.js +87 -0
  26. package/dist/utils.js.map +1 -0
  27. package/package.json +7 -3
  28. package/src/app.ts +79 -0
  29. package/src/export.ts +288 -0
  30. package/src/index.ts +47 -91
  31. package/src/main.ts +18 -1
  32. package/src/routes/cards.ts +121 -69
  33. package/src/routes/fieldTypes.ts +4 -2
  34. package/src/routes/linkTypes.ts +6 -1
  35. package/src/routes/templates.ts +11 -1
  36. package/src/routes/tree.ts +6 -1
  37. package/src/utils.ts +100 -0
  38. package/dist/public/assets/index-DqBrVRB4.js +0 -605
  39. package/dist/public/assets/index-ImgtZ9pq.css +0 -1
  40. package/dist/routes/cardTypes.js +0 -49
  41. package/dist/routes/cardTypes.js.map +0 -1
  42. package/src/routes/cardTypes.ts +0 -54
@@ -10,7 +10,7 @@
10
10
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
11
11
  */
12
12
 
13
- import { Hono } from 'hono';
13
+ import { Context, Hono } from 'hono';
14
14
  import Processor from '@asciidoctor/core';
15
15
  import {
16
16
  Card,
@@ -20,6 +20,7 @@ import {
20
20
  } from '@cyberismo/data-handler/interfaces/project-interfaces';
21
21
  import { CommandManager, evaluateMacros } from '@cyberismo/data-handler';
22
22
  import { ContentfulStatusCode } from 'hono/utils/http-status';
23
+ import { getCardQueryResult, isSSGContext, ssgParams } from '../export.js';
23
24
 
24
25
  const router = new Hono();
25
26
 
@@ -70,6 +71,7 @@ router.get('/', async (c) => {
70
71
  async function getCardDetails(
71
72
  commands: CommandManager,
72
73
  key: string,
74
+ staticMode?: boolean,
73
75
  ): Promise<any> {
74
76
  const fetchCardDetails: ProjectFetchCardDetails = {
75
77
  attachments: true,
@@ -97,11 +99,15 @@ async function getCardDetails(
97
99
 
98
100
  let asciidocContent = '';
99
101
  try {
100
- asciidocContent = await evaluateMacros(cardDetailsResponse.content || '', {
101
- mode: 'inject',
102
- projectPath: commands.project.basePath || '',
103
- cardKey: key,
104
- });
102
+ asciidocContent = await evaluateMacros(
103
+ cardDetailsResponse.content || '',
104
+ {
105
+ mode: staticMode ? 'static' : 'inject',
106
+ project: commands.project,
107
+ cardKey: key,
108
+ },
109
+ commands.calculateCmd,
110
+ );
105
111
  } catch (error) {
106
112
  asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${asciidocContent}`;
107
113
  }
@@ -116,13 +122,16 @@ async function getCardDetails(
116
122
  })
117
123
  .toString();
118
124
 
119
- // always parse for now
120
- await commands.calculateCmd.generate();
121
-
122
- const card = await commands.calculateCmd.runQuery('card', {
123
- cardKey: key,
124
- });
125
+ // always parse for now if not in export mode
126
+ if (!staticMode) {
127
+ await commands.calculateCmd.generate();
128
+ }
125
129
 
130
+ const card = staticMode
131
+ ? await getCardQueryResult(commands.project.basePath, key)
132
+ : await commands.calculateCmd.runQuery('card', {
133
+ cardKey: key,
134
+ });
126
135
  if (card.length !== 1) {
127
136
  throw new Error('Query failed. Check card-query syntax');
128
137
  }
@@ -157,22 +166,44 @@ async function getCardDetails(
157
166
  * 500:
158
167
  * description: project_path not set.
159
168
  */
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
- }
169
+ router.get(
170
+ '/:key',
171
+ ssgParams(async (c: Context) => {
172
+ const commands = c.get('commands');
173
+ const fetchedCards = await commands.showCmd.showCards(
174
+ CardLocation.projectOnly,
175
+ );
176
+ const projectCards = fetchedCards.find(
177
+ (cardContainer) => cardContainer.type === 'project',
178
+ );
179
+ if (!projectCards) {
180
+ throw new Error('Data handler did not return project cards');
181
+ }
182
+ return projectCards.cards.map((key) => ({ key }));
183
+ }),
184
+ async (c) => {
185
+ const key = c.req.param('key');
186
+ if (!key) {
187
+ return c.text('No search key', 400);
188
+ }
165
189
 
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,
190
+ const result = await getCardDetails(
191
+ c.get('commands'),
192
+ key,
193
+ isSSGContext(c),
173
194
  );
174
- }
175
- });
195
+ if (result.status === 200) {
196
+ return c.json(result.data);
197
+ } else {
198
+ return c.json(
199
+ {
200
+ error: result.message || 'Unknown error',
201
+ },
202
+ result.status as ContentfulStatusCode,
203
+ );
204
+ }
205
+ },
206
+ );
176
207
 
177
208
  /**
178
209
  * @swagger
@@ -271,8 +302,10 @@ router.patch('/:key', async (c) => {
271
302
  if (result.status === 200) {
272
303
  return c.json(result.data);
273
304
  } else {
274
- return c.text(
275
- result.message || 'Unknown error',
305
+ return c.json(
306
+ {
307
+ error: result.message || 'Unknown error',
308
+ },
276
309
  result.status as ContentfulStatusCode,
277
310
  );
278
311
  }
@@ -309,8 +342,10 @@ router.delete('/:key', async (c) => {
309
342
  await commands.removeCmd.remove('card', key);
310
343
  return new Response(null, { status: 204 });
311
344
  } catch (error) {
312
- return c.text(
313
- error instanceof Error ? error.message : 'Unknown error',
345
+ return c.json(
346
+ {
347
+ error: error instanceof Error ? error.message : 'Unknown error',
348
+ },
314
349
  400,
315
350
  );
316
351
  }
@@ -361,9 +396,9 @@ router.post('/:key', async (c) => {
361
396
  return c.json(result);
362
397
  } catch (error) {
363
398
  if (error instanceof Error) {
364
- return c.text(error.message, 400);
399
+ return c.json({ error: error.message }, 400);
365
400
  }
366
- return c.text('Unknown error occurred', 500);
401
+ return c.json({ error: 'Unknown error occurred' }, 500);
367
402
  }
368
403
  });
369
404
 
@@ -563,11 +598,15 @@ router.post('/:key/parse', async (c) => {
563
598
  try {
564
599
  let asciidocContent = '';
565
600
  try {
566
- asciidocContent = await evaluateMacros(content, {
567
- mode: 'inject',
568
- projectPath: commands.project.basePath || '',
569
- cardKey: key,
570
- });
601
+ asciidocContent = await evaluateMacros(
602
+ content,
603
+ {
604
+ mode: 'inject',
605
+ project: commands.project,
606
+ cardKey: key,
607
+ },
608
+ commands.calculateCmd,
609
+ );
571
610
  } catch (error) {
572
611
  asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
573
612
  }
@@ -725,43 +764,56 @@ router.delete('/:key/links', async (c) => {
725
764
  * 500:
726
765
  * description: project_path not set.
727
766
  */
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);
767
+ router.get(
768
+ '/:key/a/:attachment',
769
+ ssgParams(async (c: Context) => {
770
+ const commands = c.get('commands');
771
+ const attachments = await commands.showCmd.showAttachments();
772
+ return attachments.map((attachment) => ({
773
+ key: attachment.card,
774
+ attachment: attachment.fileName,
775
+ }));
776
+ }),
777
+ async (c) => {
778
+ const commands = c.get('commands');
779
+ const { key, attachment } = c.req.param();
780
+ const filename = decodeURI(attachment);
781
+
782
+ if (!filename || !key) {
783
+ return c.text('Missing cardKey or filename', 400);
784
+ }
732
785
 
733
- if (!filename || !key) {
734
- return c.text('Missing cardKey or filename', 400);
735
- }
786
+ try {
787
+ const attachmentResponse = await commands.showCmd.showAttachment(
788
+ key,
789
+ filename,
790
+ );
736
791
 
737
- try {
738
- const attachmentResponse = await commands.showCmd.showAttachment(
739
- key,
740
- filename,
741
- );
792
+ if (!attachmentResponse) {
793
+ return c.text(
794
+ `No attachment found from card ${key} and filename ${filename}`,
795
+ 404,
796
+ );
797
+ }
742
798
 
743
- if (!attachmentResponse) {
744
- return c.text(
745
- `No attachment found from card ${key} and filename ${filename}`,
799
+ const payload = attachmentResponse as any;
800
+
801
+ return new Response(payload.fileBuffer, {
802
+ headers: {
803
+ 'Content-Type': payload.mimeType,
804
+ 'Content-Disposition': `attachment; filename="${filename}"`,
805
+ 'Cache-Control': 'no-store',
806
+ },
807
+ });
808
+ } catch {
809
+ return c.json(
810
+ {
811
+ error: `No attachment found from card ${key} and filename ${filename}`,
812
+ },
746
813
  404,
747
814
  );
748
815
  }
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
- });
816
+ },
817
+ );
766
818
 
767
819
  export default router;
@@ -41,8 +41,10 @@ router.get('/', async (c) => {
41
41
 
42
42
  return c.json(fieldTypes);
43
43
  } else {
44
- return c.text(
45
- `No field types found from path ${c.get('projectPath')}`,
44
+ return c.json(
45
+ {
46
+ error: `No field types found from path ${c.get('projectPath')}`,
47
+ },
46
48
  500,
47
49
  );
48
50
  }
@@ -41,7 +41,12 @@ router.get('/', async (c) => {
41
41
 
42
42
  return c.json(linkTypes);
43
43
  } else {
44
- return c.text(`No link types found from path ${c.get('projectPath')}`, 500);
44
+ return c.json(
45
+ {
46
+ error: `No link types found from path ${c.get('projectPath')}`,
47
+ },
48
+ 500,
49
+ );
45
50
  }
46
51
  });
47
52
 
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { Hono } from 'hono';
14
+ import { isSSGContext } from 'hono/ssg';
14
15
 
15
16
  const router = new Hono();
16
17
 
@@ -29,13 +30,22 @@ const router = new Hono();
29
30
  * description: project_path not set or other internal error
30
31
  */
31
32
  router.get('/', async (c) => {
33
+ // We do not need templates in ssg context
34
+ if (isSSGContext(c)) {
35
+ return c.json([]);
36
+ }
32
37
  const commands = c.get('commands');
33
38
 
34
39
  const response = await commands.showCmd.showTemplatesWithDetails();
35
40
  if (response) {
36
41
  return c.json(response);
37
42
  } else {
38
- return c.text(`No templates found from path ${c.get('projectPath')}`, 500);
43
+ return c.json(
44
+ {
45
+ error: `No templates found from path ${c.get('projectPath')}`,
46
+ },
47
+ 500,
48
+ );
39
49
  }
40
50
  });
41
51
 
@@ -33,7 +33,12 @@ router.get('/', async (c) => {
33
33
  const tree = await commands.calculateCmd.runQuery('tree');
34
34
  return c.json(tree);
35
35
  } catch (e) {
36
- return c.text((e instanceof Error && e.message) || '', 500);
36
+ return c.json(
37
+ {
38
+ error: (e instanceof Error && e.message) || '',
39
+ },
40
+ 500,
41
+ );
37
42
  }
38
43
  });
39
44
 
package/src/utils.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2025
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+ import path from 'node:path';
14
+ import { createServer } from 'node:net';
15
+
16
+ /**
17
+ * Runs promises in parallel, but only maxConcurrent at a time.
18
+ * @param promises - Array of promises to run in parallel
19
+ * @param maxConcurrent - Maximum number of promises to run at a time
20
+ * @returns - Promise that resolves when all promises have resolved
21
+ */
22
+ export async function runInParallel<T>(
23
+ promises: (() => Promise<unknown>)[],
24
+ maxConcurrent: number = 2,
25
+ ) {
26
+ const waitingPromises: (() => Promise<unknown>)[] = [];
27
+ const wrappedPromises = promises.map((fn) => async () => {
28
+ await fn();
29
+ const next = waitingPromises.shift();
30
+ if (next) {
31
+ await next();
32
+ }
33
+ });
34
+
35
+ const runningPromises = wrappedPromises.slice(0, maxConcurrent);
36
+ waitingPromises.push(...wrappedPromises.slice(maxConcurrent));
37
+
38
+ return Promise.all(runningPromises.map((p) => p()));
39
+ }
40
+ /**
41
+ * Runs a callback and returns the result or undefined if it throws.
42
+ * @param cb - The callback to run.
43
+ * @returns The result of the callback or undefined if it throws.
44
+ */
45
+ export async function runCbSafely<T>(
46
+ cb: () => Promise<T> | T,
47
+ ): Promise<T | undefined> {
48
+ try {
49
+ return await cb();
50
+ } catch {}
51
+ }
52
+
53
+ /**
54
+ * The relative path to the static frontend directory.
55
+ */
56
+ export const staticFrontendDirRelative = path.relative(
57
+ process.cwd(),
58
+ path.resolve(import.meta.dirname, 'public'),
59
+ );
60
+
61
+ /**
62
+ * Finds a free port.
63
+ * @param port - The port to start looking for.
64
+ * @param maxAttempts - The maximum number of attempts to find a free port.
65
+ * @returns The free port.
66
+ */
67
+ export async function findFreePort(
68
+ minPort: number,
69
+ maxPort: number,
70
+ ): Promise<number> {
71
+ for (let i = minPort; i < maxPort; i++) {
72
+ try {
73
+ await testPort(i);
74
+ return i;
75
+ } catch (err) {
76
+ if (err instanceof Error && err.message.includes('EADDRINUSE')) {
77
+ console.log(`Port ${i} is already in use, trying next port...`);
78
+ } else {
79
+ throw err;
80
+ }
81
+ }
82
+ }
83
+ throw new Error('Failed to find free port');
84
+ }
85
+
86
+ function testPort(port: number) {
87
+ return new Promise((resolve, reject) => {
88
+ const server = createServer();
89
+ server.listen(port, () => {
90
+ server.close();
91
+ resolve(true);
92
+ });
93
+ server.on('error', (err) => {
94
+ reject(err);
95
+ });
96
+ setTimeout(() => {
97
+ reject(new Error('Timed out waiting for port to be free'));
98
+ }, 2000);
99
+ });
100
+ }