ts-forge 1.0.2 → 2.0.0-beta.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/CHANGELOG.md +10 -0
- package/README.md +379 -51
- package/dist/decorators/resolver.js +1 -2
- package/dist/decorators/resolverFn.js +46 -88
- package/dist/utils/getDefinitionsForClass.js +6 -9
- package/package.json +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## v2.0.0-beta.0
|
|
2
|
+
|
|
3
|
+
- Resolved warnings from TypeScript 6.0
|
|
4
|
+
|
|
5
|
+
## v1.0.3
|
|
6
|
+
|
|
7
|
+
- README.md: updated documentation
|
|
8
|
+
- Keywords were added to package.json
|
|
9
|
+
- Fixed vulnerabilities in package.json
|
|
10
|
+
|
|
1
11
|
## v1.0.2
|
|
2
12
|
|
|
3
13
|
- Files in the .github and scripts folders were added to .npmignore to avoid publishing them to npm registry
|
package/README.md
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
# ts-forge
|
|
2
|
+
|
|
3
|
+
[npm version](https://badge.fury.io/js/ts-forge)
|
|
4
|
+
[npm downloads](https://www.npmjs.com/package/ts-forge)
|
|
2
5
|
|
|
3
6
|
This library allows you to define resolvers for your Atlassian Forge addon using decorators. It uses the `@forge/resolver` library under the hood to define resolvers, but it provides a more convenient way to define them using decorators.
|
|
4
7
|
|
|
5
8
|
## Table of contents
|
|
6
9
|
|
|
7
10
|
- [Introduction](#introduction)
|
|
8
|
-
- [
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
9
12
|
- [Installation](#installation)
|
|
13
|
+
- [TypeScript Configuration](#typescript-configuration)
|
|
10
14
|
- [@Resolver](#resolver)
|
|
11
15
|
- [@ResolverFn](#resolverfn)
|
|
12
16
|
- [getDefinitionsForClass](#getdefinitionsforclass)
|
|
13
17
|
- [Middlewares](#middlewares)
|
|
14
18
|
- [Error handler](#error-handler)
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
[
|
|
19
|
+
- [Frontend Usage](#frontend-usage)
|
|
20
|
+
- [Complete Example](#complete-example)
|
|
21
|
+
- [License](#license)
|
|
18
22
|
|
|
19
23
|
## Introduction
|
|
20
24
|
|
|
@@ -25,14 +29,14 @@ It provides two main decorators: `@Resolver` and `@ResolverFn`, which can be use
|
|
|
25
29
|
When you create a resolver, you typically would do something like this:
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
|
-
import Resolver "@forge/resolver";
|
|
32
|
+
import Resolver from "@forge/resolver";
|
|
29
33
|
|
|
30
34
|
const resolver = new Resolver();
|
|
31
35
|
|
|
32
36
|
resolver.define("hello", async (req) => {
|
|
33
37
|
try {
|
|
34
38
|
return { message: "Hello world!" };
|
|
35
|
-
} catch(error)
|
|
39
|
+
} catch (error) {
|
|
36
40
|
console.error("An error occurred:", error);
|
|
37
41
|
|
|
38
42
|
return { status: "error", message: "An error occurred" };
|
|
@@ -44,7 +48,7 @@ resolver.define("update-user", async (req) => {
|
|
|
44
48
|
// Update user logic...
|
|
45
49
|
|
|
46
50
|
return { status: "success", message: "User updated successfully" };
|
|
47
|
-
} catch(error) {
|
|
51
|
+
} catch (error) {
|
|
48
52
|
console.error("An error occurred:", error);
|
|
49
53
|
|
|
50
54
|
return { status: "error", message: "An error occurred" };
|
|
@@ -69,7 +73,6 @@ class UserResolver {
|
|
|
69
73
|
@ResolverFn("update-user")
|
|
70
74
|
async updateUser(req) {
|
|
71
75
|
// Update user logic...
|
|
72
|
-
}
|
|
73
76
|
return { status: "success", message: "User updated successfully" };
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -82,16 +85,101 @@ export const definitions = getDefinitionsForClass({
|
|
|
82
85
|
});
|
|
83
86
|
```
|
|
84
87
|
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
1. Install the library and its peer dependency:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install ts-forge @forge/resolver
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
1. Configure TypeScript (see [TypeScript Configuration](#typescript-configuration))
|
|
97
|
+
2. Create a resolver class:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { Resolver, ResolverFn } from "ts-forge";
|
|
101
|
+
import { Request } from "@forge/resolver";
|
|
102
|
+
|
|
103
|
+
@Resolver()
|
|
104
|
+
class MyResolver {
|
|
105
|
+
@ResolverFn("say-hello")
|
|
106
|
+
async sayHello(req: Request) {
|
|
107
|
+
return { message: "Hello from ts-forge!" };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
1. Export definitions in any file you want, for example `src/resolvers/index.ts`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { getDefinitionsForClass } from "ts-forge";
|
|
116
|
+
import MyResolver from "./MyResolver";
|
|
117
|
+
|
|
118
|
+
export const handler = getDefinitionsForClass({
|
|
119
|
+
resolvers: [MyResolver]
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
1. Define your Forge function in your manifest.yml:
|
|
124
|
+
|
|
125
|
+
```yml
|
|
126
|
+
modules:
|
|
127
|
+
function:
|
|
128
|
+
- key: resolver-function
|
|
129
|
+
handler: resolvers/index.handler
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
1. Use in your Forge app's UI by invoking the resolver function:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { invoke } from "@forge/bridge";
|
|
136
|
+
|
|
137
|
+
async function callHelloResolver() {
|
|
138
|
+
const response = await invoke("resolver-function", {
|
|
139
|
+
key: "say-hello"
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
console.log(response.message); // "Hello from ts-forge!"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
85
146
|
## Installation
|
|
86
147
|
|
|
87
148
|
This is a Node.js module available on npm. Make sure you have `@forge/resolver` installed as well, since this library uses it under the hood. The minimum version of `@forge/resolver` required is `^1.6.0`.
|
|
88
149
|
|
|
89
150
|
```bash
|
|
90
|
-
npm install ts-forge
|
|
151
|
+
npm install ts-forge @forge/resolver
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or if you're using yarn:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
yarn add ts-forge @forge/resolver
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or if you're using pnpm:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pnpm add ts-forge @forge/resolver
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## TypeScript Configuration
|
|
167
|
+
|
|
168
|
+
To use decorators in TypeScript, you need to enable experimental decorators in your `tsconfig.json`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"compilerOptions": {
|
|
173
|
+
"experimentalDecorators": true,
|
|
174
|
+
"emitDecoratorMetadata": true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
91
177
|
```
|
|
92
178
|
|
|
93
179
|
## @Resolver
|
|
94
180
|
|
|
181
|
+
The `@Resolver` decorator is used to mark a class as a resolver. You can optionally provide configuration for middlewares and error handlers that will apply to all resolver functions in the class.
|
|
182
|
+
|
|
95
183
|
```ts
|
|
96
184
|
import { Resolver, ResolverFn } from "ts-forge";
|
|
97
185
|
|
|
@@ -112,10 +200,12 @@ Parameter 1 (Resolver config) - Optional
|
|
|
112
200
|
- `middlewares: ((request: Request) => any | Promise<any>)[]`
|
|
113
201
|
- Optional
|
|
114
202
|
- This is an array of middleware functions, each function will be called before the resolver function
|
|
115
|
-
- If
|
|
203
|
+
- If a middleware returns a value, it will be sent to the frontend and pending middlewares and the resolver function won't be called
|
|
116
204
|
|
|
117
205
|
## @ResolverFn
|
|
118
206
|
|
|
207
|
+
The `@ResolverFn` decorator is used to mark a method as a resolver function. Each decorated method will be registered as a resolver that can be invoked from your Forge app's frontend.
|
|
208
|
+
|
|
119
209
|
```ts
|
|
120
210
|
import { Request } from "@forge/resolver";
|
|
121
211
|
import { Resolver, ResolverFn } from "ts-forge";
|
|
@@ -133,7 +223,7 @@ class HelloWorldResolver {
|
|
|
133
223
|
|
|
134
224
|
**Parameter 1 (Resolver key or resolver function config) - Required**
|
|
135
225
|
|
|
136
|
-
You
|
|
226
|
+
You can use either a string or an object.
|
|
137
227
|
|
|
138
228
|
**Resolver key**
|
|
139
229
|
|
|
@@ -150,23 +240,25 @@ You could use either, a string or an object
|
|
|
150
240
|
- `middlewares: ((request: Request) => any | Promise<any>)[]`
|
|
151
241
|
- Optional
|
|
152
242
|
- This is an array of middleware functions, each function will be called before the resolver function
|
|
153
|
-
- If
|
|
243
|
+
- If a middleware returns a value, it will be sent to the frontend and pending middlewares and the resolver function won't be called
|
|
154
244
|
- `errorHandler: (error: any, request: Request) => any | Promise<any>`
|
|
155
245
|
- Optional
|
|
156
246
|
- If an error is thrown in your resolver function or middlewares, this function will be called so that you can handle the error and return a response
|
|
157
247
|
|
|
158
|
-
The `@ResolverFn()` decorator takes only one argument, which can be either a string or an object
|
|
159
|
-
If you don't need to define middlewares or an error handler, you
|
|
248
|
+
The `@ResolverFn()` decorator takes only one argument, which can be either a string or an object.
|
|
249
|
+
If you don't need to define middlewares or an error handler, you can just pass the resolver key:
|
|
160
250
|
|
|
161
251
|
```ts
|
|
162
252
|
@Resolver()
|
|
163
253
|
class HelloWorldResolver {
|
|
164
254
|
@ResolverFn("my-resolver")
|
|
165
|
-
async hello(req: Request) {
|
|
255
|
+
async hello(req: Request) {
|
|
256
|
+
return { message: "Hello!" };
|
|
257
|
+
}
|
|
166
258
|
}
|
|
167
259
|
```
|
|
168
260
|
|
|
169
|
-
|
|
261
|
+
Or, if you need to define middlewares or an error handler for that resolver function, you can pass a config object like this:
|
|
170
262
|
|
|
171
263
|
```ts
|
|
172
264
|
@Resolver()
|
|
@@ -176,38 +268,42 @@ class HelloWorldResolver {
|
|
|
176
268
|
middlewares: [authMiddleware, adminMiddleware],
|
|
177
269
|
errorHandler: myErrorHandler
|
|
178
270
|
})
|
|
179
|
-
async hello(req: Request) {
|
|
271
|
+
async hello(req: Request) {
|
|
272
|
+
return { message: "Hello!" };
|
|
273
|
+
}
|
|
180
274
|
}
|
|
181
275
|
```
|
|
182
276
|
|
|
183
277
|
## getDefinitionsForClass
|
|
184
278
|
|
|
185
|
-
This function is used to get definitions of the resolvers that you created
|
|
279
|
+
This function is used to get definitions of the resolvers that you created. It uses `@forge/resolver` under the hood to define resolvers, then calls `getDefinitions()` method and returns its result. The returned value should be exported as the `handler` for your Forge function.
|
|
186
280
|
|
|
187
281
|
```ts
|
|
188
282
|
import { getDefinitionsForClass } from "ts-forge";
|
|
189
283
|
|
|
190
|
-
getDefinitionsForClass({
|
|
284
|
+
const handler = getDefinitionsForClass({
|
|
191
285
|
resolvers: [HelloWorldResolver, GetJiraProjectResolver],
|
|
192
|
-
errorHandler:
|
|
193
|
-
middlewares:
|
|
286
|
+
errorHandler: myErrorHandler,
|
|
287
|
+
middlewares: [authMiddleware]
|
|
194
288
|
});
|
|
289
|
+
|
|
290
|
+
export { handler };
|
|
195
291
|
```
|
|
196
292
|
|
|
197
293
|
#### Parameters
|
|
198
294
|
|
|
199
295
|
**Parameter 1 (Config) - Required**
|
|
200
296
|
|
|
201
|
-
- `{ resolvers: [], middlewares
|
|
297
|
+
- `{ resolvers: [], middlewares?: MiddlewareFn[], errorHandler?: ErrorHandlerFn }`
|
|
202
298
|
- `resolvers`
|
|
203
299
|
- Required
|
|
204
|
-
- This is an array of
|
|
300
|
+
- This is an array of classes (not instances) that have been decorated with `@Resolver()`
|
|
205
301
|
- Must have at least one element
|
|
206
302
|
- `middlewares: ((request: Request) => any | Promise<any>)[]`
|
|
207
303
|
- Optional
|
|
208
304
|
- This is an array of middleware functions, each function will be called before the resolver function
|
|
209
|
-
- If
|
|
210
|
-
-
|
|
305
|
+
- If a middleware returns a value, it will be sent to the frontend and pending middlewares and the resolver function won't be called
|
|
306
|
+
- These middlewares will be called before each resolver function defined in `resolvers`
|
|
211
307
|
- **Note:** Middlewares defined here will be the last to be called, in case you have defined middlewares in `@Resolver()` or `@ResolverFn()`
|
|
212
308
|
- `errorHandler: (error: any, request: Request) => any | Promise<any>`
|
|
213
309
|
- Optional
|
|
@@ -215,32 +311,57 @@ getDefinitionsForClass({
|
|
|
215
311
|
- This function will handle errors for all the resolver functions defined in `resolvers`
|
|
216
312
|
- **Note:** This function will be called only when an error handler was not defined in `@Resolver()` and `@ResolverFn()`
|
|
217
313
|
|
|
314
|
+
#### Return Value
|
|
315
|
+
|
|
316
|
+
Returns the definitions object that should be exported for use in your Forge manifest. This object contains all the resolver function definitions that can be invoked from your Forge app's frontend.
|
|
317
|
+
|
|
218
318
|
## Middlewares
|
|
219
319
|
|
|
220
|
-
You can define middlewares in `@ResolverFn()`, `@Resolver()` and `getDefinitionsForClass()`.
|
|
320
|
+
You can define middlewares in `@ResolverFn()`, `@Resolver()` and `getDefinitionsForClass()`. Each middleware takes one parameter which is the same request object you have access to in resolver functions.
|
|
221
321
|
|
|
222
|
-
Middleware functions have priority depending where they were defined. When a resolver function is invoked, the middlewares will be called in the following order:
|
|
322
|
+
Middleware functions have priority depending on where they were defined. When a resolver function is invoked, the middlewares will be called in the following order:
|
|
223
323
|
|
|
224
324
|
1. `@ResolverFn()`: Middlewares defined in this decorator's config will be the first to be called
|
|
225
325
|
2. `@Resolver()`: Middlewares defined in this decorator's config will run after `@ResolverFn()`'s middlewares are called. If no middlewares were defined in `@ResolverFn()`, middlewares defined in `@Resolver()` would be the first to be called
|
|
226
|
-
3. `getDefinitionsForClass()`: Middlewares defined in this function will be the last to be called. If no middlewares were defined in `@ResolverFn()` and `@Resolver()`, middlewares defined in `getDefinitionsForClass()` would be the first to be called
|
|
326
|
+
3. `getDefinitionsForClass()`: Middlewares defined in this function will be the last to be called. If no middlewares were defined in `@ResolverFn()` and `@Resolver()`, middlewares defined in `getDefinitionsForClass()` would be the first to be called. This is useful when you want the same middlewares to be called for all of your resolver functions, so that you don't have to define the same middlewares in each `@ResolverFn()` or `@Resolver()`
|
|
327
|
+
|
|
328
|
+
**Important**: If a middleware returns a value, that value will be sent to the frontend immediately and any pending middlewares and the resolver function will not be executed. If a middleware should just perform checks or logging without stopping execution, it should not return anything.
|
|
227
329
|
|
|
228
330
|
#### Usage
|
|
229
331
|
|
|
230
|
-
This is an example of a middleware function
|
|
332
|
+
This is an example of a middleware function that returns early (blocking execution):
|
|
231
333
|
|
|
232
334
|
```ts
|
|
233
335
|
import { Request } from "@forge/resolver";
|
|
234
336
|
|
|
235
|
-
//
|
|
337
|
+
// This middleware returns a value, stopping execution if user is not admin
|
|
236
338
|
function verifyUserIsJiraAdmin(request: Request): any | Promise<any> {
|
|
237
|
-
|
|
339
|
+
const isAdmin = checkIfUserIsAdmin(request.context.accountId);
|
|
340
|
+
|
|
341
|
+
if (!isAdmin) {
|
|
342
|
+
// Returning a value stops execution
|
|
343
|
+
return { error: "Unauthorized", message: "Admin access required" };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Not returning anything allows execution to continue
|
|
238
347
|
}
|
|
239
348
|
```
|
|
240
349
|
|
|
241
|
-
|
|
350
|
+
Example of a middleware that doesn't return (passes through):
|
|
242
351
|
|
|
243
|
-
|
|
352
|
+
```ts
|
|
353
|
+
import { Request } from "@forge/resolver";
|
|
354
|
+
|
|
355
|
+
// This middleware logs but doesn't return, so execution continues
|
|
356
|
+
function loggingMiddleware(request: Request): void {
|
|
357
|
+
console.log(`Resolver called: ${request.payload?.action}`);
|
|
358
|
+
// No return statement - execution continues to next middleware/resolver
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Then you can use these middlewares in `@ResolverFn()`, `@Resolver()` or `getDefinitionsForClass()`.
|
|
363
|
+
|
|
364
|
+
In the following example the `verifyUserIsJiraAdmin` middleware is used in `@ResolverFn()`. This is useful when you want certain middleware to be called for specific resolver functions:
|
|
244
365
|
|
|
245
366
|
```ts
|
|
246
367
|
@Resolver()
|
|
@@ -254,21 +375,18 @@ class HelloWorldResolver {
|
|
|
254
375
|
}
|
|
255
376
|
|
|
256
377
|
// verifyUserIsJiraAdmin won't be called when this resolver function is invoked
|
|
257
|
-
@ResolverFn(
|
|
258
|
-
key: "get-current-user",
|
|
259
|
-
middlewares: []
|
|
260
|
-
})
|
|
378
|
+
@ResolverFn("get-current-user")
|
|
261
379
|
async getCurrentUser() {
|
|
262
380
|
// Return current user
|
|
263
381
|
}
|
|
264
382
|
}
|
|
265
383
|
```
|
|
266
384
|
|
|
267
|
-
In this example the `verifyUserIsJiraAdmin` middleware is used in `@Resolver()`. Middlewares defined in this decorator will be called for all resolver functions defined in the class
|
|
385
|
+
In this example the `verifyUserIsJiraAdmin` middleware is used in `@Resolver()`. Middlewares defined in this decorator will be called for all resolver functions defined in the class:
|
|
268
386
|
|
|
269
387
|
```ts
|
|
270
388
|
@Resolver({
|
|
271
|
-
// Middlewares defined in this array will be called
|
|
389
|
+
// Middlewares defined in this array will be called for each resolver function defined in this class
|
|
272
390
|
middlewares: [verifyUserIsJiraAdmin]
|
|
273
391
|
})
|
|
274
392
|
class HelloWorldResolver {
|
|
@@ -286,7 +404,7 @@ class HelloWorldResolver {
|
|
|
286
404
|
}
|
|
287
405
|
```
|
|
288
406
|
|
|
289
|
-
Last but not least, you can define middlewares in the `getDefinitionsForClass` function. Middlewares defined here will be called
|
|
407
|
+
Last but not least, you can define middlewares in the `getDefinitionsForClass` function. Middlewares defined here will be called for all resolvers that you pass to the `resolvers` array:
|
|
290
408
|
|
|
291
409
|
```ts
|
|
292
410
|
@Resolver()
|
|
@@ -305,12 +423,12 @@ class UserResolver {
|
|
|
305
423
|
}
|
|
306
424
|
}
|
|
307
425
|
|
|
308
|
-
const
|
|
426
|
+
const handler = getDefinitionsForClass({
|
|
309
427
|
middlewares: [verifyUserIsJiraAdmin],
|
|
310
428
|
// Middlewares will be called before each resolver function, you don't need to add the verifyUserIsJiraAdmin middleware
|
|
311
429
|
// in @Resolver or @ResolverFn decorators
|
|
312
430
|
resolvers: [HelloWorldResolver, UserResolver]
|
|
313
|
-
})
|
|
431
|
+
});
|
|
314
432
|
```
|
|
315
433
|
|
|
316
434
|
## Error handler
|
|
@@ -322,13 +440,13 @@ When an error is thrown, the error handler will be called with the error and the
|
|
|
322
440
|
When an error is thrown, the error handler will be called in the following order:
|
|
323
441
|
|
|
324
442
|
1. If an error handler is defined in the `@ResolverFn()` decorator, it will be called
|
|
325
|
-
2. Error handler defined in `@Resolver()
|
|
443
|
+
2. Error handler defined in `@Resolver()` will be called when an error handler was not defined in `@ResolverFn()`
|
|
326
444
|
3. If no error handler was defined in `@ResolverFn()` and `@Resolver()`, the error handler defined in `getDefinitionsForClass()` will be called
|
|
327
445
|
4. If no error handler was defined in any of the above, the error will be logged to the console and will be returned to the frontend
|
|
328
446
|
|
|
329
447
|
#### Usage
|
|
330
448
|
|
|
331
|
-
This is an example of an error handler function
|
|
449
|
+
This is an example of an error handler function:
|
|
332
450
|
|
|
333
451
|
```ts
|
|
334
452
|
import { Request } from "@forge/resolver";
|
|
@@ -341,8 +459,9 @@ function myErrorHandler(error: any, request: Request): any | Promise<any> {
|
|
|
341
459
|
}
|
|
342
460
|
```
|
|
343
461
|
|
|
344
|
-
Then you can use this error handler in `@ResolverFn()`, `@Resolver()` or `getDefinitionsForClass()
|
|
345
|
-
|
|
462
|
+
Then you can use this error handler in `@ResolverFn()`, `@Resolver()` or `getDefinitionsForClass()`.
|
|
463
|
+
|
|
464
|
+
In the following example the `myErrorHandler` error handler is used in `@ResolverFn()`. This is useful when you want a specific error handler to be called for some resolver functions:
|
|
346
465
|
|
|
347
466
|
```ts
|
|
348
467
|
@Resolver()
|
|
@@ -363,11 +482,11 @@ class HelloWorldResolver {
|
|
|
363
482
|
}
|
|
364
483
|
```
|
|
365
484
|
|
|
366
|
-
In this example the `myErrorHandler` error handler is used in `@Resolver()`. Error handlers defined in this decorator will be called for all resolver functions defined in the class
|
|
485
|
+
In this example the `myErrorHandler` error handler is used in `@Resolver()`. Error handlers defined in this decorator will be called for all resolver functions defined in the class:
|
|
367
486
|
|
|
368
487
|
```ts
|
|
369
488
|
@Resolver({
|
|
370
|
-
// Error handler defined in this
|
|
489
|
+
// Error handler defined in this decorator will be called if an error is thrown in any of the resolver functions defined in this class
|
|
371
490
|
errorHandler: myErrorHandler
|
|
372
491
|
})
|
|
373
492
|
class HelloWorldResolver {
|
|
@@ -385,7 +504,7 @@ class HelloWorldResolver {
|
|
|
385
504
|
}
|
|
386
505
|
```
|
|
387
506
|
|
|
388
|
-
Last but not least, you can define an error handler in the `getDefinitionsForClass` function. Error handlers defined here will be called if an error is thrown in any of the resolvers that you pass to the `resolvers` array
|
|
507
|
+
Last but not least, you can define an error handler in the `getDefinitionsForClass` function. Error handlers defined here will be called if an error is thrown in any of the resolvers that you pass to the `resolvers` array:
|
|
389
508
|
|
|
390
509
|
```ts
|
|
391
510
|
@Resolver()
|
|
@@ -404,10 +523,219 @@ class UserResolver {
|
|
|
404
523
|
}
|
|
405
524
|
}
|
|
406
525
|
|
|
407
|
-
const
|
|
526
|
+
const handler = getDefinitionsForClass({
|
|
408
527
|
errorHandler: myErrorHandler,
|
|
409
528
|
// Error handler will be called if an error is thrown in any of the resolver functions
|
|
410
529
|
// You don't need to add the myErrorHandler error handler in @Resolver or @ResolverFn decorators
|
|
411
530
|
resolvers: [HelloWorldResolver, UserResolver]
|
|
412
531
|
});
|
|
413
532
|
```
|
|
533
|
+
|
|
534
|
+
## Frontend Usage
|
|
535
|
+
|
|
536
|
+
After defining your resolvers and exporting the handler, you can invoke them from your Forge app's frontend using the `invoke()` method from `@forge/bridge`.
|
|
537
|
+
|
|
538
|
+
**Backend (src/resolvers/index.ts):**
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
import { getDefinitionsForClass, Resolver, ResolverFn } from "ts-forge";
|
|
542
|
+
import { Request } from "@forge/resolver";
|
|
543
|
+
|
|
544
|
+
@Resolver()
|
|
545
|
+
class UserResolver {
|
|
546
|
+
@ResolverFn("get-user")
|
|
547
|
+
async getUser(req: Request) {
|
|
548
|
+
const userId = req.payload.userId;
|
|
549
|
+
// Fetch user logic...
|
|
550
|
+
return { id: userId, name: "John Doe" };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@ResolverFn("update-user")
|
|
554
|
+
async updateUser(req: Request) {
|
|
555
|
+
const { userId, name } = req.payload;
|
|
556
|
+
// Update user logic...
|
|
557
|
+
return { success: true, message: "User updated" };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export const handler = getDefinitionsForClass({
|
|
562
|
+
resolvers: [UserResolver]
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Frontend (src/index.tsx or src/index.jsx):**
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import React, { useState, useEffect } from "react";
|
|
570
|
+
import { invoke } from "@forge/bridge";
|
|
571
|
+
|
|
572
|
+
function App() {
|
|
573
|
+
const [user, setUser] = useState(null);
|
|
574
|
+
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
// Invoke the "get-user" resolver
|
|
577
|
+
invoke("get-user", { userId: "123" })
|
|
578
|
+
.then((response) => {
|
|
579
|
+
setUser(response);
|
|
580
|
+
})
|
|
581
|
+
.catch((error) => {
|
|
582
|
+
console.error("Error fetching user:", error);
|
|
583
|
+
});
|
|
584
|
+
}, []);
|
|
585
|
+
|
|
586
|
+
const handleUpdateUser = async () => {
|
|
587
|
+
try {
|
|
588
|
+
// Invoke the "update-user" resolver
|
|
589
|
+
const response = await invoke("update-user", {
|
|
590
|
+
userId: "123",
|
|
591
|
+
name: "Jane Doe"
|
|
592
|
+
});
|
|
593
|
+
console.log(response.message);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error("Error updating user:", error);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<div>
|
|
601
|
+
{user && (
|
|
602
|
+
<>
|
|
603
|
+
<h1>{user.name}</h1>
|
|
604
|
+
<button onClick={handleUpdateUser}>Update User</button>
|
|
605
|
+
</>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export default App;
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## Complete Example
|
|
615
|
+
|
|
616
|
+
Here's a complete example showing all features together:
|
|
617
|
+
|
|
618
|
+
**src/middlewares/auth.ts:**
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
import { Request } from "@forge/resolver";
|
|
622
|
+
|
|
623
|
+
export function authMiddleware(request: Request) {
|
|
624
|
+
const accountId = request.context.accountId;
|
|
625
|
+
|
|
626
|
+
if (!accountId) {
|
|
627
|
+
return { error: "Unauthorized", message: "User not authenticated" };
|
|
628
|
+
}
|
|
629
|
+
// Don't return anything to continue execution
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function adminMiddleware(request: Request) {
|
|
633
|
+
// Check if user is admin
|
|
634
|
+
const isAdmin = checkIfUserIsAdmin(request.context.accountId);
|
|
635
|
+
|
|
636
|
+
if (!isAdmin) {
|
|
637
|
+
return { error: "Forbidden", message: "Admin access required" };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**src/handlers/errorHandler.ts:**
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
import { Request } from "@forge/resolver";
|
|
646
|
+
|
|
647
|
+
export function globalErrorHandler(error: any, request: Request) {
|
|
648
|
+
console.error("Error in resolver:", error);
|
|
649
|
+
|
|
650
|
+
// Log to external service
|
|
651
|
+
logErrorToService(error, request.context);
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: "An unexpected error occurred",
|
|
656
|
+
message: error.message
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**src/resolvers/UserResolver.ts:**
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import { Resolver, ResolverFn } from "ts-forge";
|
|
665
|
+
import { Request } from "@forge/resolver";
|
|
666
|
+
import { authMiddleware, adminMiddleware } from "../middlewares/auth";
|
|
667
|
+
|
|
668
|
+
@Resolver({
|
|
669
|
+
middlewares: [authMiddleware]
|
|
670
|
+
})
|
|
671
|
+
class UserResolver {
|
|
672
|
+
@ResolverFn("get-current-user")
|
|
673
|
+
async getCurrentUser(req: Request) {
|
|
674
|
+
const accountId = req.context.accountId;
|
|
675
|
+
// Fetch user from API
|
|
676
|
+
return { id: accountId, name: "John Doe" };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@ResolverFn({
|
|
680
|
+
key: "delete-user",
|
|
681
|
+
middlewares: [adminMiddleware] // Additional middleware for this resolver
|
|
682
|
+
})
|
|
683
|
+
async deleteUser(req: Request) {
|
|
684
|
+
const userId = req.payload.userId;
|
|
685
|
+
// Delete user logic
|
|
686
|
+
return { success: true, message: `User ${userId} deleted` };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export default UserResolver;
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**src/resolvers/ProjectResolver.ts:**
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import { Resolver, ResolverFn } from "ts-forge";
|
|
697
|
+
import { Request } from "@forge/resolver";
|
|
698
|
+
|
|
699
|
+
@Resolver()
|
|
700
|
+
class ProjectResolver {
|
|
701
|
+
@ResolverFn("get-projects")
|
|
702
|
+
async getProjects(req: Request) {
|
|
703
|
+
// Fetch projects
|
|
704
|
+
return { projects: [] };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export default ProjectResolver;
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**src/resolvers/index.ts:**
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import { getDefinitionsForClass } from "ts-forge";
|
|
715
|
+
import UserResolver from "./UserResolver";
|
|
716
|
+
import ProjectResolver from "./ProjectResolver";
|
|
717
|
+
import { globalErrorHandler } from "../handlers/errorHandler";
|
|
718
|
+
|
|
719
|
+
export const handler = getDefinitionsForClass({
|
|
720
|
+
resolvers: [UserResolver, ProjectResolver],
|
|
721
|
+
errorHandler: globalErrorHandler
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**manifest.yml:**
|
|
726
|
+
|
|
727
|
+
```yml
|
|
728
|
+
modules:
|
|
729
|
+
function:
|
|
730
|
+
- key: resolver-function
|
|
731
|
+
handler: resolvers/index.handler
|
|
732
|
+
jira:issuePanel:
|
|
733
|
+
- key: my-issue-panel
|
|
734
|
+
function: resolver-function
|
|
735
|
+
title: My Issue Panel
|
|
736
|
+
icon: https://example.com/icon.png
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
## License
|
|
740
|
+
|
|
741
|
+
MIT
|
|
@@ -15,8 +15,7 @@ import _ from "../constants";
|
|
|
15
15
|
* class MyResolver{}
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export function Resolver(config) {
|
|
19
|
-
if (config === void 0) { config = { middlewares: [], errorHandler: undefined }; }
|
|
18
|
+
export function Resolver(config = { middlewares: [], errorHandler: undefined }) {
|
|
20
19
|
return function (constructor) {
|
|
21
20
|
constructor.prototype[_.RESOLVER_CONFIG] = {
|
|
22
21
|
middlewares: Array.from(config.middlewares || []),
|
|
@@ -7,33 +7,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
11
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
12
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
13
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
14
|
-
function step(op) {
|
|
15
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
16
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
17
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
18
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
19
|
-
switch (op[0]) {
|
|
20
|
-
case 0: case 1: t = op; break;
|
|
21
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
22
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
23
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
24
|
-
default:
|
|
25
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
26
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
27
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
28
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
29
|
-
if (t[2]) _.ops.pop();
|
|
30
|
-
_.trys.pop(); continue;
|
|
31
|
-
}
|
|
32
|
-
op = body.call(thisArg, _);
|
|
33
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
34
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
10
|
import isResolverFnConfig from "../utils/isResolverFnConfig";
|
|
38
11
|
import _ from "../constants";
|
|
39
12
|
/**
|
|
@@ -46,7 +19,7 @@ import _ from "../constants";
|
|
|
46
19
|
export function ResolverFn(resolverFnConfig) {
|
|
47
20
|
return function (target, propertyKey, descriptor) {
|
|
48
21
|
// Handle the case where resolverFnConfig is a string
|
|
49
|
-
|
|
22
|
+
const config = isResolverFnConfig(resolverFnConfig)
|
|
50
23
|
? resolverFnConfig
|
|
51
24
|
: { key: resolverFnConfig, middlewares: [], errorHandler: undefined };
|
|
52
25
|
// If the middlewares property is not an array, initialize it as an empty array
|
|
@@ -59,76 +32,61 @@ export function ResolverFn(resolverFnConfig) {
|
|
|
59
32
|
}
|
|
60
33
|
// Add the resolver's data to the target
|
|
61
34
|
target[_.RESOLVER_FUNCTIONS].push({
|
|
62
|
-
config
|
|
35
|
+
config,
|
|
63
36
|
methodName: propertyKey
|
|
64
37
|
});
|
|
65
|
-
|
|
38
|
+
const method = descriptor.value;
|
|
66
39
|
descriptor.value = function (req) {
|
|
67
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
_i++;
|
|
101
|
-
return [3 /*break*/, 2];
|
|
102
|
-
case 5: return [4 /*yield*/, method.call(this, req)];
|
|
103
|
-
case 6:
|
|
104
|
-
// Call the original method
|
|
105
|
-
return [2 /*return*/, _a.sent()];
|
|
106
|
-
case 7:
|
|
107
|
-
error_1 = _a.sent();
|
|
108
|
-
errorHandlerFn = config.errorHandler || (targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.errorHandler) || (targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.globalErrorHandler);
|
|
109
|
-
if (!errorHandlerFn) return [3 /*break*/, 11];
|
|
110
|
-
_a.label = 8;
|
|
111
|
-
case 8:
|
|
112
|
-
_a.trys.push([8, 10, , 11]);
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const targetConfig = this[_.RESOLVER_CONFIG] || {};
|
|
42
|
+
try {
|
|
43
|
+
// Merge middlewares from the resolver function and the resolver class
|
|
44
|
+
// Method middlewares are always executed first
|
|
45
|
+
const middlewares = Array.from((config === null || config === void 0 ? void 0 : config.middlewares) || []);
|
|
46
|
+
// If there are middlewares defined in the resolver class, add them to the middlewares array
|
|
47
|
+
if (Array.isArray(targetConfig.middlewares)) {
|
|
48
|
+
middlewares.push(...targetConfig.middlewares);
|
|
49
|
+
}
|
|
50
|
+
// If there are middlewares defined in the getDefinitionsForClass config, add them to the middlewares array
|
|
51
|
+
if (Array.isArray(targetConfig.globalMiddlewares)) {
|
|
52
|
+
middlewares.push(...targetConfig.globalMiddlewares);
|
|
53
|
+
}
|
|
54
|
+
// Run provided middlewares
|
|
55
|
+
for (const middleware of middlewares) {
|
|
56
|
+
const response = yield middleware(req);
|
|
57
|
+
// If response is provided, send it to the frontend
|
|
58
|
+
// This means that the resolver or next middleware will not be called
|
|
59
|
+
if (response) {
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Call the original method
|
|
64
|
+
return yield method.call(this, req);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Resolver function error handler has priority over the resolver error handler
|
|
68
|
+
const errorHandlerFn = config.errorHandler || (targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.errorHandler) || (targetConfig === null || targetConfig === void 0 ? void 0 : targetConfig.globalErrorHandler);
|
|
69
|
+
// If an error handler is provided, call it with the request data and error object
|
|
70
|
+
// This allows the error handler to handle the error and return a response
|
|
71
|
+
if (errorHandlerFn) {
|
|
72
|
+
try {
|
|
113
73
|
req.context.resolver = {
|
|
114
74
|
key: config.key,
|
|
115
75
|
className: target.constructor.name,
|
|
116
76
|
methodName: propertyKey
|
|
117
77
|
};
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
err_1 = _a.sent();
|
|
78
|
+
return yield errorHandlerFn(error, req);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
122
81
|
// If the error handler throws an error, log it and return the original error
|
|
123
|
-
console.error(
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
// If no error handler is provided, log the error, then return it
|
|
127
|
-
console.error(error_1);
|
|
128
|
-
return [2 /*return*/, error_1];
|
|
129
|
-
case 12: return [2 /*return*/];
|
|
82
|
+
console.error(err);
|
|
83
|
+
return error;
|
|
84
|
+
}
|
|
130
85
|
}
|
|
131
|
-
|
|
86
|
+
// If no error handler is provided, log the error, then return it
|
|
87
|
+
console.error(error);
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
132
90
|
});
|
|
133
91
|
};
|
|
134
92
|
};
|
|
@@ -8,23 +8,21 @@ import _ from "../constants";
|
|
|
8
8
|
* @param config.errorHandler - Custom error handler to be applied to the provided resolvers
|
|
9
9
|
* @returns - Resolver definitions
|
|
10
10
|
*/
|
|
11
|
-
export function getDefinitionsForClass(
|
|
12
|
-
|
|
13
|
-
var forgeResolver = new ForgeResolver();
|
|
11
|
+
export function getDefinitionsForClass({ resolvers, middlewares = [], errorHandler }) {
|
|
12
|
+
const forgeResolver = new ForgeResolver();
|
|
14
13
|
if (!Array.isArray(middlewares)) {
|
|
15
14
|
throw new Error("Middlewares must be an array");
|
|
16
15
|
}
|
|
17
|
-
if (middlewares.length > 0 && !middlewares.every(
|
|
16
|
+
if (middlewares.length > 0 && !middlewares.every((m) => typeof m === "function")) {
|
|
18
17
|
throw new Error("All middlewares must be functions");
|
|
19
18
|
}
|
|
20
19
|
if (errorHandler && typeof errorHandler !== "function") {
|
|
21
20
|
throw new Error("Error handler must be a function");
|
|
22
21
|
}
|
|
23
|
-
for (
|
|
24
|
-
var instance = resolvers_1[_i];
|
|
22
|
+
for (const instance of resolvers) {
|
|
25
23
|
if (instance[_.RESOLVER_CONFIG]) {
|
|
26
24
|
// Set global middlewares and error handler for the resolver class
|
|
27
|
-
|
|
25
|
+
const instanceConfig = {
|
|
28
26
|
middlewares: Array.from(instance[_.RESOLVER_CONFIG].middlewares || []),
|
|
29
27
|
errorHandler: instance[_.RESOLVER_CONFIG].errorHandler
|
|
30
28
|
};
|
|
@@ -34,8 +32,7 @@ export function getDefinitionsForClass(_a) {
|
|
|
34
32
|
// Set the updated config on the instance
|
|
35
33
|
instance[_.RESOLVER_CONFIG] = instanceConfig;
|
|
36
34
|
}
|
|
37
|
-
for (
|
|
38
|
-
var resolver = _d[_c];
|
|
35
|
+
for (const resolver of instance[_.RESOLVER_FUNCTIONS]) {
|
|
39
36
|
forgeResolver.define(resolver.config.key, instance[resolver.methodName].bind(instance));
|
|
40
37
|
}
|
|
41
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-forge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest --run --coverage",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"keywords": [
|
|
17
17
|
"forge",
|
|
18
18
|
"atlassian",
|
|
19
|
-
"addon"
|
|
19
|
+
"addon",
|
|
20
|
+
"ts-forge",
|
|
21
|
+
"resolvers"
|
|
20
22
|
],
|
|
21
23
|
"author": "Gustavo Velazquez",
|
|
22
24
|
"license": "ISC",
|
|
@@ -26,10 +28,10 @@
|
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@types/node": "^24.10.0",
|
|
29
|
-
"@vitest/coverage-v8": "4.
|
|
30
|
-
"@vitest/ui": "4.
|
|
31
|
-
"jsdom": "^
|
|
32
|
-
"typescript": "
|
|
33
|
-
"vitest": "^4.
|
|
31
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
32
|
+
"@vitest/ui": "4.1.2",
|
|
33
|
+
"jsdom": "^29.0.1",
|
|
34
|
+
"typescript": "6.0.2",
|
|
35
|
+
"vitest": "^4.1.2"
|
|
34
36
|
}
|
|
35
|
-
}
|
|
37
|
+
}
|