@cyberismo/backend 0.0.21 → 0.0.23

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 (152) hide show
  1. package/dist/app.d.ts +5 -2
  2. package/dist/app.js +25 -10
  3. package/dist/app.js.map +1 -1
  4. package/dist/auth/index.d.ts +16 -0
  5. package/dist/auth/index.js +15 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/keycloak.d.ts +27 -0
  8. package/dist/auth/keycloak.js +81 -0
  9. package/dist/auth/keycloak.js.map +1 -0
  10. package/dist/auth/mock.d.ts +23 -0
  11. package/dist/auth/mock.js +28 -0
  12. package/dist/auth/mock.js.map +1 -0
  13. package/dist/auth/types.d.ts +16 -0
  14. package/dist/auth/types.js +14 -0
  15. package/dist/auth/types.js.map +1 -0
  16. package/dist/domain/auth/index.d.ts +14 -0
  17. package/dist/domain/auth/index.js +30 -0
  18. package/dist/domain/auth/index.js.map +1 -0
  19. package/dist/domain/calculations/index.js +3 -1
  20. package/dist/domain/calculations/index.js.map +1 -1
  21. package/dist/domain/calculations/service.js +13 -11
  22. package/dist/domain/calculations/service.js.map +1 -1
  23. package/dist/domain/cardTypes/index.js +5 -3
  24. package/dist/domain/cardTypes/index.js.map +1 -1
  25. package/dist/domain/cardTypes/service.js +24 -72
  26. package/dist/domain/cardTypes/service.js.map +1 -1
  27. package/dist/domain/cards/index.js +124 -25
  28. package/dist/domain/cards/index.js.map +1 -1
  29. package/dist/domain/cards/lib.js +92 -93
  30. package/dist/domain/cards/lib.js.map +1 -1
  31. package/dist/domain/cards/presence.d.ts +50 -0
  32. package/dist/domain/cards/presence.js +93 -0
  33. package/dist/domain/cards/presence.js.map +1 -0
  34. package/dist/domain/cards/schema.d.ts +47 -0
  35. package/dist/domain/cards/schema.js +37 -0
  36. package/dist/domain/cards/schema.js.map +1 -0
  37. package/dist/domain/cards/service.d.ts +7 -3
  38. package/dist/domain/cards/service.js +81 -91
  39. package/dist/domain/cards/service.js.map +1 -1
  40. package/dist/domain/connectors/index.d.ts +15 -0
  41. package/dist/domain/connectors/index.js +37 -0
  42. package/dist/domain/connectors/index.js.map +1 -0
  43. package/dist/domain/connectors/service.d.ts +23 -0
  44. package/dist/domain/connectors/service.js +46 -0
  45. package/dist/domain/connectors/service.js.map +1 -0
  46. package/dist/domain/fieldTypes/index.js +4 -2
  47. package/dist/domain/fieldTypes/index.js.map +1 -1
  48. package/dist/domain/graphModels/index.js +3 -1
  49. package/dist/domain/graphModels/index.js.map +1 -1
  50. package/dist/domain/graphViews/index.js +3 -1
  51. package/dist/domain/graphViews/index.js.map +1 -1
  52. package/dist/domain/labels/index.js +4 -2
  53. package/dist/domain/labels/index.js.map +1 -1
  54. package/dist/domain/labels/service.d.ts +1 -1
  55. package/dist/domain/labels/service.js +2 -2
  56. package/dist/domain/labels/service.js.map +1 -1
  57. package/dist/domain/linkTypes/index.js +4 -2
  58. package/dist/domain/linkTypes/index.js.map +1 -1
  59. package/dist/domain/logicPrograms/index.js +3 -1
  60. package/dist/domain/logicPrograms/index.js.map +1 -1
  61. package/dist/domain/mcp/index.d.ts +15 -0
  62. package/dist/domain/mcp/index.js +127 -0
  63. package/dist/domain/mcp/index.js.map +1 -0
  64. package/dist/domain/project/index.js +19 -6
  65. package/dist/domain/project/index.js.map +1 -1
  66. package/dist/domain/project/schema.d.ts +3 -0
  67. package/dist/domain/project/schema.js +8 -0
  68. package/dist/domain/project/schema.js.map +1 -1
  69. package/dist/domain/project/service.d.ts +3 -1
  70. package/dist/domain/project/service.js +24 -14
  71. package/dist/domain/project/service.js.map +1 -1
  72. package/dist/domain/reports/index.js +3 -1
  73. package/dist/domain/reports/index.js.map +1 -1
  74. package/dist/domain/resources/index.js +6 -4
  75. package/dist/domain/resources/index.js.map +1 -1
  76. package/dist/domain/resources/service.js +66 -64
  77. package/dist/domain/resources/service.js.map +1 -1
  78. package/dist/domain/templates/index.js +5 -3
  79. package/dist/domain/templates/index.js.map +1 -1
  80. package/dist/domain/tree/index.js +3 -1
  81. package/dist/domain/tree/index.js.map +1 -1
  82. package/dist/domain/tree/service.js +0 -1
  83. package/dist/domain/tree/service.js.map +1 -1
  84. package/dist/domain/workflows/index.js +3 -1
  85. package/dist/domain/workflows/index.js.map +1 -1
  86. package/dist/export.d.ts +6 -5
  87. package/dist/export.js +16 -13
  88. package/dist/export.js.map +1 -1
  89. package/dist/index.d.ts +8 -2
  90. package/dist/index.js +12 -4
  91. package/dist/index.js.map +1 -1
  92. package/dist/main.js +29 -2
  93. package/dist/main.js.map +1 -1
  94. package/dist/middleware/auth.d.ts +40 -0
  95. package/dist/middleware/auth.js +68 -0
  96. package/dist/middleware/auth.js.map +1 -0
  97. package/dist/middleware/commandManager.d.ts +2 -2
  98. package/dist/middleware/commandManager.js +9 -11
  99. package/dist/middleware/commandManager.js.map +1 -1
  100. package/dist/public/THIRD-PARTY.txt +1212 -605
  101. package/dist/public/assets/index-Cdn_jRWy.js +720 -0
  102. package/dist/public/assets/index-ypsafPwV.css +1 -0
  103. package/dist/public/config.json +1 -0
  104. package/dist/public/images/broken_link.svg +7 -0
  105. package/dist/public/index.html +2 -2
  106. package/dist/types.d.ts +25 -0
  107. package/dist/types.js +13 -1
  108. package/dist/types.js.map +1 -1
  109. package/package.json +10 -7
  110. package/src/app.ts +37 -15
  111. package/src/auth/index.ts +17 -0
  112. package/src/auth/keycloak.ts +109 -0
  113. package/src/auth/mock.ts +38 -0
  114. package/src/auth/types.ts +18 -0
  115. package/src/domain/auth/index.ts +35 -0
  116. package/src/domain/calculations/index.ts +13 -6
  117. package/src/domain/calculations/service.ts +16 -14
  118. package/src/domain/cardTypes/index.ts +24 -16
  119. package/src/domain/cardTypes/service.ts +41 -95
  120. package/src/domain/cards/index.ts +258 -90
  121. package/src/domain/cards/lib.ts +102 -100
  122. package/src/domain/cards/presence.ts +124 -0
  123. package/src/domain/cards/schema.ts +41 -0
  124. package/src/domain/cards/service.ts +138 -93
  125. package/src/domain/connectors/index.ts +39 -0
  126. package/src/domain/connectors/service.ts +67 -0
  127. package/src/domain/fieldTypes/index.ts +23 -16
  128. package/src/domain/graphModels/index.ts +13 -6
  129. package/src/domain/graphViews/index.ts +13 -6
  130. package/src/domain/labels/index.ts +5 -2
  131. package/src/domain/labels/service.ts +2 -2
  132. package/src/domain/linkTypes/index.ts +14 -7
  133. package/src/domain/logicPrograms/index.ts +3 -0
  134. package/src/domain/mcp/index.ts +159 -0
  135. package/src/domain/project/index.ts +40 -9
  136. package/src/domain/project/schema.ts +9 -0
  137. package/src/domain/project/service.ts +37 -17
  138. package/src/domain/reports/index.ts +13 -6
  139. package/src/domain/resources/index.ts +6 -1
  140. package/src/domain/resources/service.ts +102 -97
  141. package/src/domain/templates/index.ts +31 -19
  142. package/src/domain/tree/index.ts +3 -1
  143. package/src/domain/tree/service.ts +0 -1
  144. package/src/domain/workflows/index.ts +13 -6
  145. package/src/export.ts +17 -15
  146. package/src/index.ts +18 -7
  147. package/src/main.ts +44 -2
  148. package/src/middleware/auth.ts +90 -0
  149. package/src/middleware/commandManager.ts +11 -14
  150. package/src/types.ts +27 -0
  151. package/dist/public/assets/index-CRSBseQM.css +0 -1
  152. package/dist/public/assets/index-Ca10XaMv.js +0 -164156
@@ -30,122 +30,124 @@ export async function getCardDetails(
30
30
  staticMode: boolean,
31
31
  raw: boolean,
32
32
  ): Promise<result> {
33
- let cardDetailsResponse: Card;
34
- try {
35
- cardDetailsResponse = commands.showCmd.showCardDetails(key);
36
- } catch {
37
- return { status: 400, message: `Card ${key} not found from project` };
38
- }
33
+ return commands.consistent(async () => {
34
+ let cardDetailsResponse: Card;
35
+ try {
36
+ cardDetailsResponse = await commands.showCmd.showCardDetails(key);
37
+ } catch {
38
+ return { status: 400, message: `Card ${key} not found from project` };
39
+ }
39
40
 
40
- if (!cardDetailsResponse) {
41
- return { status: 400, message: `Card ${key} not found from project` };
42
- }
41
+ if (!cardDetailsResponse) {
42
+ return { status: 400, message: `Card ${key} not found from project` };
43
+ }
43
44
 
44
- // always parse for now if not in export mode
45
- if (!staticMode && !raw) {
46
- await commands.calculateCmd.generate();
47
- }
45
+ let asciidocContent = '';
46
+ try {
47
+ asciidocContent = await evaluateMacros(
48
+ cardDetailsResponse.content || '',
49
+ {
50
+ context: staticMode ? 'exportedSite' : 'localApp',
51
+ mode: staticMode ? 'staticSite' : 'inject',
52
+ project: commands.project,
53
+ cardKey: key,
54
+ },
55
+ );
56
+ } catch (error) {
57
+ asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${asciidocContent}`;
58
+ }
48
59
 
49
- let asciidocContent = '';
50
- try {
51
- asciidocContent = await evaluateMacros(cardDetailsResponse.content || '', {
52
- context: staticMode ? 'exportedSite' : 'localApp',
53
- mode: staticMode ? 'staticSite' : 'inject',
54
- project: commands.project,
55
- cardKey: key,
56
- });
57
- } catch (error) {
58
- asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${asciidocContent}`;
59
- }
60
+ const htmlContent = Processor()
61
+ .convert(asciidocContent, {
62
+ safe: 'safe',
63
+ attributes: {
64
+ imagesdir: `/api/cards/${key}/a`,
65
+ icons: 'font',
66
+ },
67
+ })
68
+ .toString();
60
69
 
61
- const htmlContent = Processor()
62
- .convert(asciidocContent, {
63
- safe: 'safe',
64
- attributes: {
65
- imagesdir: `/api/cards/${key}/a`,
66
- icons: 'font',
67
- },
68
- })
69
- .toString();
70
+ if (raw) {
71
+ if (!cardDetailsResponse.metadata) {
72
+ throw new Error('Card has no metadata');
73
+ }
74
+ const cardType = await commands.showCmd.showResource(
75
+ cardDetailsResponse.metadata.cardType,
76
+ 'cardTypes',
77
+ );
70
78
 
71
- if (raw) {
72
- if (!cardDetailsResponse.metadata) {
73
- throw new Error('Card has no metadata');
79
+ const fields = [];
80
+ let i = 0;
81
+ for (const customField of cardType.customFields) {
82
+ const fieldType = await commands.showCmd.showResource(
83
+ customField.name,
84
+ 'fieldTypes',
85
+ );
86
+ fields.push({
87
+ key: customField.name,
88
+ visibility: 'always',
89
+ index: i++,
90
+ fieldDisplayName: fieldType.displayName,
91
+ fieldDescription: fieldType.description,
92
+ dataType: fieldType.dataType,
93
+ isCalculated: customField.isCalculated,
94
+ value: cardDetailsResponse.metadata[customField.name],
95
+ enumValues: fieldType.enumValues ?? [],
96
+ });
97
+ }
98
+ return {
99
+ status: 200,
100
+ data: {
101
+ key: cardDetailsResponse.key,
102
+ rank: cardDetailsResponse.metadata?.rank,
103
+ title: cardDetailsResponse.metadata?.title || '',
104
+ cardType: cardDetailsResponse.metadata?.cardType || '',
105
+ cardTypeDisplayName: cardDetailsResponse.metadata.cardType,
106
+ workflowState: '',
107
+ lastUpdated: cardDetailsResponse.metadata.lastUpdated,
108
+ createdAt: cardDetailsResponse.metadata.createdAt,
109
+ fields,
110
+ labels: cardDetailsResponse.metadata?.labels || [],
111
+ links: [],
112
+ notifications: [],
113
+ policyChecks: {
114
+ successes: [],
115
+ failures: [],
116
+ },
117
+ deniedOperations: {
118
+ transition: [],
119
+ move: [],
120
+ delete: [],
121
+ editField: [],
122
+ editContent: [],
123
+ },
124
+ rawContent: cardDetailsResponse.content || '',
125
+ parsedContent: htmlContent,
126
+ attachments: cardDetailsResponse.attachments,
127
+ },
128
+ };
74
129
  }
75
- const cardType = await commands.showCmd.showResource(
76
- cardDetailsResponse.metadata.cardType,
77
- 'cardTypes',
78
- );
79
130
 
80
- const fields = [];
81
- let i = 0;
82
- for (const customField of cardType.customFields) {
83
- const fieldType = await commands.showCmd.showResource(
84
- customField.name,
85
- 'fieldTypes',
86
- );
87
- fields.push({
88
- key: customField.name,
89
- visibility: 'always',
90
- index: i++,
91
- fieldDisplayName: fieldType.displayName,
92
- fieldDescription: fieldType.description,
93
- dataType: fieldType.dataType,
94
- isCalculated: customField.isCalculated,
95
- value: cardDetailsResponse.metadata[customField.name],
96
- enumValues: fieldType.enumValues ?? [],
97
- });
131
+ const card = staticMode
132
+ ? await getCardQueryResult(commands, key)
133
+ : await commands.calculateCmd.runQuery('card', 'localApp', {
134
+ cardKey: key,
135
+ });
136
+
137
+ if (card.length !== 1) {
138
+ throw new Error('Query failed. Check card-query syntax');
98
139
  }
140
+
99
141
  return {
100
142
  status: 200,
101
143
  data: {
102
- key: cardDetailsResponse.key,
103
- rank: cardDetailsResponse.metadata?.rank,
104
- title: cardDetailsResponse.metadata?.title || '',
105
- cardType: cardDetailsResponse.metadata?.cardType || '',
106
- cardTypeDisplayName: cardDetailsResponse.metadata.cardType,
107
- workflowState: '',
108
- lastUpdated: cardDetailsResponse.metadata.lastUpdated,
109
- fields,
110
- labels: cardDetailsResponse.metadata?.labels || [],
111
- links: [],
112
- notifications: [],
113
- policyChecks: {
114
- successes: [],
115
- failures: [],
116
- },
117
- deniedOperations: {
118
- transition: [],
119
- move: [],
120
- delete: [],
121
- editField: [],
122
- editContent: [],
123
- },
144
+ ...card[0],
124
145
  rawContent: cardDetailsResponse.content || '',
125
146
  parsedContent: htmlContent,
126
147
  attachments: cardDetailsResponse.attachments,
127
148
  },
128
149
  };
129
- }
130
-
131
- const card = staticMode
132
- ? await getCardQueryResult(commands.project.basePath, key)
133
- : await commands.calculateCmd.runQuery('card', 'localApp', {
134
- cardKey: key,
135
- });
136
- if (card.length !== 1) {
137
- throw new Error('Query failed. Check card-query syntax');
138
- }
139
-
140
- return {
141
- status: 200,
142
- data: {
143
- ...card[0],
144
- rawContent: cardDetailsResponse.content || '',
145
- parsedContent: htmlContent,
146
- attachments: cardDetailsResponse.attachments,
147
- },
148
- };
150
+ });
149
151
  }
150
152
  /**
151
153
  * Returns all cards from a tree query, flattened.
@@ -0,0 +1,124 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
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. This program is distributed in the hope that it
7
+ will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
8
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ See the GNU Affero General Public License for more details.
10
+ 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
+
14
+ import type { UserInfo } from '../../types.js';
15
+ import type { SSEMessage } from 'hono/streaming';
16
+
17
+ export interface PresenceEntry {
18
+ userId: string;
19
+ userName: string;
20
+ mode: 'viewing' | 'editing';
21
+ }
22
+
23
+ interface Connection {
24
+ user: UserInfo;
25
+ mode: 'viewing' | 'editing';
26
+ send: (message: SSEMessage) => void;
27
+ }
28
+
29
+ /**
30
+ * In-memory presence tracker for card editing/viewing.
31
+ * Tracks which users are currently looking at or editing each card,
32
+ * and notifies all connected clients via SSE when presence changes.
33
+ */
34
+ class PresenceStore {
35
+ // cardKey -> connectionId -> Connection
36
+ private connections = new Map<string, Map<string, Connection>>();
37
+ private nextId = 0;
38
+
39
+ /**
40
+ * Add a user connection for a card. Returns a connection ID for later removal.
41
+ */
42
+ add(
43
+ cardKey: string,
44
+ user: UserInfo,
45
+ mode: 'viewing' | 'editing',
46
+ send: (message: SSEMessage) => void,
47
+ ): string {
48
+ const connId = String(this.nextId++);
49
+
50
+ if (!this.connections.has(cardKey)) {
51
+ this.connections.set(cardKey, new Map());
52
+ }
53
+
54
+ this.connections.get(cardKey)!.set(connId, { user, mode, send });
55
+ this.broadcast(cardKey);
56
+
57
+ return connId;
58
+ }
59
+
60
+ /**
61
+ * Remove a connection and broadcast updated presence.
62
+ */
63
+ remove(cardKey: string, connId: string): void {
64
+ const cardConns = this.connections.get(cardKey);
65
+ if (!cardConns) return;
66
+
67
+ cardConns.delete(connId);
68
+
69
+ if (cardConns.size === 0) {
70
+ this.connections.delete(cardKey);
71
+ } else {
72
+ this.broadcast(cardKey);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get current presence list for a card.
78
+ */
79
+ getPresence(cardKey: string): PresenceEntry[] {
80
+ const cardConns = this.connections.get(cardKey);
81
+ if (!cardConns) return [];
82
+
83
+ // Deduplicate by userId — if a user has multiple connections,
84
+ // prefer the 'editing' mode
85
+ const byUser = new Map<string, PresenceEntry>();
86
+ for (const conn of cardConns.values()) {
87
+ const existing = byUser.get(conn.user.id);
88
+ if (!existing || conn.mode === 'editing') {
89
+ byUser.set(conn.user.id, {
90
+ userId: conn.user.id,
91
+ userName: conn.user.name,
92
+ mode: conn.mode,
93
+ });
94
+ }
95
+ }
96
+
97
+ return Array.from(byUser.values());
98
+ }
99
+
100
+ /**
101
+ * Broadcast current presence to all connected clients for a card.
102
+ */
103
+ private broadcast(cardKey: string): void {
104
+ const cardConns = this.connections.get(cardKey);
105
+ if (!cardConns) return;
106
+
107
+ const presence = this.getPresence(cardKey);
108
+ const data = JSON.stringify({ editors: presence });
109
+ const message: SSEMessage = { event: 'presence', data };
110
+
111
+ for (const conn of cardConns.values()) {
112
+ conn.send(message);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Remove all connections. Intended for test cleanup.
118
+ */
119
+ removeAll(): void {
120
+ this.connections.clear();
121
+ }
122
+ }
123
+
124
+ export const presenceStore = new PresenceStore();
@@ -0,0 +1,41 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
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
+
14
+ import { z } from 'zod';
15
+
16
+ const linkDirection = z.enum(['outbound', 'inbound']);
17
+
18
+ export const createLinkSchema = z.object({
19
+ toCard: z.string(),
20
+ linkType: z.string(),
21
+ direction: linkDirection.default('outbound'),
22
+ description: z.string().optional(),
23
+ });
24
+
25
+ export const removeLinkSchema = z.object({
26
+ toCard: z.string(),
27
+ linkType: z.string(),
28
+ direction: linkDirection.default('outbound'),
29
+ description: z.string().optional(),
30
+ });
31
+
32
+ export const updateLinkSchema = z.object({
33
+ toCard: z.string(),
34
+ linkType: z.string(),
35
+ direction: linkDirection,
36
+ description: z.string().optional(),
37
+ previousToCard: z.string(),
38
+ previousLinkType: z.string(),
39
+ previousDirection: linkDirection,
40
+ previousDescription: z.string().optional(),
41
+ });
@@ -13,29 +13,32 @@
13
13
 
14
14
  import Processor from '@asciidoctor/core';
15
15
  import { type MetadataContent } from '@cyberismo/data-handler/interfaces/project-interfaces';
16
+ import type { attachmentPayload } from '@cyberismo/data-handler/interfaces/request-status-interfaces';
16
17
  import { type CommandManager, evaluateMacros } from '@cyberismo/data-handler';
17
18
  import { allCards } from './lib.js';
18
19
  import type { TreeOptions } from '../../types.js';
19
20
 
20
21
  export async function getProjectInfo(commands: CommandManager) {
21
- const projectResponse = await commands.showCmd.showProject();
22
+ return commands.consistent(async () => {
23
+ const projectResponse = await commands.showCmd.showProject();
22
24
 
23
- const workflowsResponse = await commands.showCmd.showWorkflowsWithDetails();
24
- if (!workflowsResponse) {
25
- throw new Error('No workflows found');
26
- }
25
+ const workflowsResponse = await commands.showCmd.showWorkflowsWithDetails();
26
+ if (!workflowsResponse) {
27
+ throw new Error('No workflows found');
28
+ }
27
29
 
28
- const cardTypesResponse = await commands.showCmd.showCardTypesWithDetails();
29
- if (!cardTypesResponse) {
30
- throw new Error('No card types found');
31
- }
30
+ const cardTypesResponse = await commands.showCmd.showCardTypesWithDetails();
31
+ if (!cardTypesResponse) {
32
+ throw new Error('No card types found');
33
+ }
32
34
 
33
- return {
34
- name: projectResponse.name,
35
- prefix: projectResponse.prefix,
36
- workflows: workflowsResponse,
37
- cardTypes: cardTypesResponse,
38
- };
35
+ return {
36
+ name: projectResponse.name,
37
+ prefix: projectResponse.prefix,
38
+ workflows: workflowsResponse,
39
+ cardTypes: cardTypesResponse,
40
+ };
41
+ });
39
42
  }
40
43
 
41
44
  export async function updateCard(
@@ -44,54 +47,31 @@ export async function updateCard(
44
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
48
  body: any,
46
49
  ) {
47
- const errors = [];
48
-
49
- if (body.state) {
50
- try {
50
+ await commands.atomic(async () => {
51
+ if (body.state) {
51
52
  await commands.transitionCmd.cardTransition(key, body.state);
52
- } catch (error) {
53
- if (error instanceof Error) errors.push(error.message);
54
53
  }
55
- }
56
-
57
- if (body.content != null) {
58
- try {
54
+ if (body.content != null) {
59
55
  await commands.editCmd.editCardContent(key, body.content);
60
- } catch (error) {
61
- if (error instanceof Error) errors.push(error.message);
62
56
  }
63
- }
64
-
65
- if (body.metadata) {
66
- for (const [metadataKey, metadataValue] of Object.entries(body.metadata)) {
67
- const value = metadataValue as MetadataContent;
68
-
69
- try {
70
- await commands.editCmd.editCardMetadata(key, metadataKey, value);
71
- } catch (error) {
72
- if (error instanceof Error) errors.push(error.message);
57
+ if (body.metadata) {
58
+ for (const [metadataKey, metadataValue] of Object.entries(
59
+ body.metadata,
60
+ )) {
61
+ await commands.editCmd.editCardMetadata(
62
+ key,
63
+ metadataKey,
64
+ metadataValue as MetadataContent,
65
+ );
73
66
  }
74
67
  }
75
- }
76
-
77
- if (body.parent) {
78
- try {
68
+ if (body.parent) {
79
69
  await commands.moveCmd.moveCard(key, body.parent);
80
- } catch (error) {
81
- if (error instanceof Error) errors.push(error.message);
82
70
  }
83
- }
84
- if (body.index != null) {
85
- try {
71
+ if (body.index != null) {
86
72
  await commands.moveCmd.rankByIndex(key, body.index);
87
- } catch (error) {
88
- if (error instanceof Error) errors.push(error.message);
89
73
  }
90
- }
91
-
92
- if (errors.length > 0) {
93
- throw new Error(errors.join('\n'));
94
- }
74
+ }, `Update card ${key}`);
95
75
  }
96
76
 
97
77
  export async function deleteCard(commands: CommandManager, key: string) {
@@ -119,18 +99,20 @@ export async function uploadAttachments(
119
99
  key: string,
120
100
  files: File[],
121
101
  ) {
122
- const succeeded = [];
123
- for (const file of files) {
124
- if (file instanceof File) {
125
- const buffer = await file.arrayBuffer();
126
- await commands.createCmd.createAttachment(
127
- key,
128
- file.name,
129
- Buffer.from(buffer),
130
- );
131
- succeeded.push(file.name);
102
+ const succeeded: string[] = [];
103
+ await commands.atomic(async () => {
104
+ for (const file of files) {
105
+ if (file instanceof File) {
106
+ const buffer = await file.arrayBuffer();
107
+ await commands.createCmd.createAttachment(
108
+ key,
109
+ file.name,
110
+ Buffer.from(buffer),
111
+ );
112
+ succeeded.push(file.name);
113
+ }
132
114
  }
133
- }
115
+ }, `Add attachments to ${key}`);
134
116
 
135
117
  return {
136
118
  message: 'Attachments uploaded successfully',
@@ -161,59 +143,122 @@ export async function parseContent(
161
143
  key: string,
162
144
  content: string,
163
145
  ) {
164
- let asciidocContent = '';
165
- try {
166
- asciidocContent = await evaluateMacros(content, {
167
- context: 'localApp',
168
- mode: 'inject',
169
- project: commands.project,
170
- cardKey: key,
171
- });
172
- } catch (error) {
173
- asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
174
- }
146
+ return commands.consistent(async () => {
147
+ let asciidocContent: string;
148
+ try {
149
+ asciidocContent = await evaluateMacros(content, {
150
+ context: 'localApp',
151
+ mode: 'inject',
152
+ project: commands.project,
153
+ cardKey: key,
154
+ });
155
+ } catch (error) {
156
+ asciidocContent = `Macro error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n${content}`;
157
+ }
158
+
159
+ const processor = Processor();
160
+ const parsedContent = processor
161
+ .convert(asciidocContent, {
162
+ safe: 'safe',
163
+ attributes: {
164
+ imagesdir: `/api/cards/${key}/a`,
165
+ icons: 'font',
166
+ },
167
+ })
168
+ .toString();
175
169
 
176
- const processor = Processor();
177
- const parsedContent = processor
178
- .convert(asciidocContent, {
179
- safe: 'safe',
180
- attributes: {
181
- imagesdir: `/api/cards/${key}/a`,
182
- icons: 'font',
183
- },
184
- })
185
- .toString();
186
-
187
- return { parsedContent };
170
+ return { parsedContent };
171
+ });
188
172
  }
189
173
 
190
174
  export async function createLink(
191
175
  commands: CommandManager,
192
176
  key: string,
193
- toCard: string,
177
+ target: string,
194
178
  linkType: string,
179
+ direction: 'outbound' | 'inbound' = 'outbound',
195
180
  description?: string,
196
181
  ) {
197
- await commands.createCmd.createLink(key, toCard, linkType, description);
182
+ // For outbound: key is source, target is destination
183
+ // For inbound: target is source, key is destination
184
+ const source = direction === 'outbound' ? key : target;
185
+ const destination = direction === 'outbound' ? target : key;
186
+
187
+ await commands.createCmd.createLink(
188
+ source,
189
+ destination,
190
+ linkType,
191
+ description,
192
+ direction,
193
+ );
194
+
198
195
  return { message: 'Link created successfully' };
199
196
  }
200
197
 
201
198
  export async function removeLink(
202
199
  commands: CommandManager,
203
200
  key: string,
204
- toCard: string,
201
+ target: string,
205
202
  linkType: string,
203
+ direction: 'outbound' | 'inbound' = 'outbound',
206
204
  description?: string,
207
205
  ) {
208
- await commands.removeCmd.remove('link', key, toCard, linkType, description);
206
+ // For outbound: key is source, target is destination
207
+ // For inbound: target is source, key is destination
208
+ const source = direction === 'outbound' ? key : target;
209
+ const destination = direction === 'outbound' ? target : key;
210
+ await commands.removeCmd.remove(
211
+ 'link',
212
+ source,
213
+ destination,
214
+ linkType,
215
+ description,
216
+ );
209
217
  return { message: 'Link removed successfully' };
210
218
  }
211
219
 
212
- export function getAttachment(
220
+ export async function updateLink(
213
221
  commands: CommandManager,
214
222
  key: string,
215
- filename: string,
223
+ toCard: string,
224
+ linkType: string,
225
+ direction: 'outbound' | 'inbound',
226
+ previousToCard: string,
227
+ previousLinkType: string,
228
+ previousDirection: 'outbound' | 'inbound',
229
+ linkDescription?: string,
230
+ previousLinkDescription?: string,
216
231
  ) {
232
+ // For simplicity create the new link first so that duplicate-link validation runs before
233
+ // the old link is removed. This also handles direction changes.
234
+ return commands.atomic(async () => {
235
+ await createLink(
236
+ commands,
237
+ key,
238
+ toCard,
239
+ linkType,
240
+ direction,
241
+ linkDescription,
242
+ );
243
+
244
+ await removeLink(
245
+ commands,
246
+ key,
247
+ previousToCard,
248
+ previousLinkType,
249
+ previousDirection,
250
+ previousLinkDescription,
251
+ );
252
+
253
+ return { message: 'Link updated successfully' };
254
+ }, `Update link on ${key} to ${toCard}`);
255
+ }
256
+
257
+ export async function getAttachment(
258
+ commands: CommandManager,
259
+ key: string,
260
+ filename: string,
261
+ ): Promise<attachmentPayload> {
217
262
  return commands.showCmd.showAttachment(key, filename);
218
263
  }
219
264