@trayio/cdk-dsl 0.0.1-beta
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.txt +22 -0
- package/README.md +858 -0
- package/dist/connector/operation/CompositeOperationHandler.d.ts +9 -0
- package/dist/connector/operation/CompositeOperationHandler.d.ts.map +1 -0
- package/dist/connector/operation/CompositeOperationHandler.js +10 -0
- package/dist/connector/operation/HttpOperationHandler.d.ts +83 -0
- package/dist/connector/operation/HttpOperationHandler.d.ts.map +1 -0
- package/dist/connector/operation/HttpOperationHandler.js +187 -0
- package/dist/connector/operation/OperationHandler.d.ts +134 -0
- package/dist/connector/operation/OperationHandler.d.ts.map +1 -0
- package/dist/connector/operation/OperationHandler.js +52 -0
- package/dist/connector/operation/OperationHandlerInvocation.d.ts +3 -0
- package/dist/connector/operation/OperationHandlerInvocation.d.ts.map +1 -0
- package/dist/connector/operation/OperationHandlerInvocation.js +2 -0
- package/dist/connector/operation/OperationHandlerSetup.d.ts +50 -0
- package/dist/connector/operation/OperationHandlerSetup.d.ts.map +1 -0
- package/dist/connector/operation/OperationHandlerSetup.js +148 -0
- package/dist/connector/operation/OperationHandlerTest.d.ts +135 -0
- package/dist/connector/operation/OperationHandlerTest.d.ts.map +1 -0
- package/dist/connector/operation/OperationHandlerTest.js +238 -0
- package/dist/connector/operation/OperationHandlerValidation.d.ts +42 -0
- package/dist/connector/operation/OperationHandlerValidation.d.ts.map +1 -0
- package/dist/connector/operation/OperationHandlerValidation.js +63 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# Connector Development Kit (CDK) DSL
|
|
2
|
+
|
|
3
|
+
The CDK Domain Specific Language (DSL) is the main component of Tray's CDK, it is used to define all the aspects of a connectors, including the behaviour of its operations.
|
|
4
|
+
|
|
5
|
+
A CDK connector consists of code written only using the DSL, which is declarative, so it only describes the connector, that description is then interpreted by the runtime
|
|
6
|
+
to execute a connector's operations.
|
|
7
|
+
|
|
8
|
+
## Documentation
|
|
9
|
+
|
|
10
|
+
The [Tray.io Developer Portal](https://developer.tray.io) has the most up to date documentation including a [quick start guide](https://developer.tray.io/developer-portal/cdk/quickstart/) for building connectors with the CDK
|
|
11
|
+
|
|
12
|
+
## Project Structure
|
|
13
|
+
|
|
14
|
+
A CDK project is just a regular npm typescript project, preconfigured with all the dependencies, linter rules and compiler options that will be used to build the connector during deployment, so it is not recommended to change those.
|
|
15
|
+
|
|
16
|
+
Other than the package.json, jest configuration and typescript configuration, a connector will have the following:
|
|
17
|
+
|
|
18
|
+
- A `connector.json` file that includes metadata about the connector, such as the name, version, title, etc
|
|
19
|
+
- A `src` directory that has:
|
|
20
|
+
- An Authentication typescript file that contains the type of the `auth` property of the `ctx` context object that operations receive together with the input, this type is the same for all operation
|
|
21
|
+
- A `test.ctx.json` json file that contains a context value (including the auth) that can be used for tests.
|
|
22
|
+
**This file should not be committed to a repository as it will have sensitive information such as access tokens**
|
|
23
|
+
- One folder per operation
|
|
24
|
+
|
|
25
|
+
## Context and Authentication
|
|
26
|
+
|
|
27
|
+
The context value (`ctx`) is received together with inputs in handlers, it contains values that are common to all operations, the main one being the `auth` property which usually contain things like tokens to identify the user making the request to a third party service.
|
|
28
|
+
|
|
29
|
+
Not all connectors need authentications, in that case, a `never` type can be used (with an empty value in the authentication test file), which is what the init command generates by default.
|
|
30
|
+
|
|
31
|
+
## Operations
|
|
32
|
+
|
|
33
|
+
A connector will have one folder per operation under the `src` folder, this folder will contain the following files:
|
|
34
|
+
|
|
35
|
+
- `operation.json` which is an object that has operation metadata, like the operation name, title (which are mandatory) and the description.
|
|
36
|
+
- `input.ts` which contains the type of the input of the operation
|
|
37
|
+
- `output.ts` which contains the type of the output of the operation
|
|
38
|
+
- `handler.ts` which is where the logic of the operation is
|
|
39
|
+
- `handler.test.ts` which contains the test cases for testing the operation's behaviour defined in the handler
|
|
40
|
+
|
|
41
|
+
## Input & Output
|
|
42
|
+
|
|
43
|
+
Each operation has an `input.ts` and `output.ts` that defines the input and output parameters for the operation. It should export this as a type named `<OperationName>Input` or `<OperationName>Output`.
|
|
44
|
+
|
|
45
|
+
During the deployment of a connector these input/output types are converted to JSON schema which powers how the Tray builder UI renders the input fields. This conversion also happens locally when you run the `tray-cdk build` command should you wish to inspect the generated JSON schema.
|
|
46
|
+
|
|
47
|
+
### Additional Input configuration
|
|
48
|
+
|
|
49
|
+
The following sections outline the number of different ways to render your input fields in Tray's builder UI by adding JSdoc annotations to the properties in your `input.ts`.
|
|
50
|
+
|
|
51
|
+
#### Data types
|
|
52
|
+
|
|
53
|
+
- string: represents string values
|
|
54
|
+
- number: represents a number, e.g. 42. If you want to limit an input field to a whole number you can use the JSdoc annotation `@TJS-type integer` above the number.
|
|
55
|
+
- boolean: is for the two values true and false
|
|
56
|
+
- array: represented by `[]`, e.g. number[]
|
|
57
|
+
- object: represented by object that contains different fields
|
|
58
|
+
|
|
59
|
+
Here is an example use of the different data types:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export type ExampleOperationInput = {
|
|
63
|
+
str: string;
|
|
64
|
+
num: number;
|
|
65
|
+
/**
|
|
66
|
+
* @TJS-type integer
|
|
67
|
+
*/
|
|
68
|
+
int: number;
|
|
69
|
+
bool: boolean;
|
|
70
|
+
obj: object;
|
|
71
|
+
arr: object[];
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Enums (Static dropdown lists)
|
|
76
|
+
|
|
77
|
+
Enums are rendered in the Tray builder UI as dropdowns. The user will see the enum display names and the enum values are what will be passed into the handler. By default, user friendly enum display names are generated. e.g. an enum of value `my-enum-value` will be rendered in the UI with the display name `My enum value`.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export type ExampleOperationInput = {
|
|
81
|
+
type: ActionType;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
enum ActionType {
|
|
85
|
+
first-option = 'first-option', // User will see an auto generated display name: First option
|
|
86
|
+
second-option = 'second-option', // User will see an auto generated display name: Second option
|
|
87
|
+
third-option = 'third-option', // User will see an auto generated display name: Third option
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
You may want to provide custom enum display names instead, to do so use the JSdoc annotation `@enumLabels` followed by a comma separated list of strings that matches the order you have defined your enums in.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
export type ExampleOperationInput = {
|
|
95
|
+
type: ActionType;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @enumLabels The First Option, The Second Option, The Third Option
|
|
100
|
+
*/
|
|
101
|
+
enum ActionType {
|
|
102
|
+
first-option = 'first-option',
|
|
103
|
+
second-option = 'second-option',
|
|
104
|
+
third-option = 'third-option',
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### DDL (Dynamic dropdown lists)
|
|
109
|
+
|
|
110
|
+
Sometimes you want to provide the user a dropdown list, but you don't know the items to display in the list because they come from another API. For example you may want to display a list of user names in a dropdown and based on the selection send the user ID. DDL's solve this problem. Once you have created an operation for your DDL (see more on this in the Composite Implementation section) you can use this DDL in the input of many other operations.
|
|
111
|
+
|
|
112
|
+
The JSdoc annotation `@lookupOperation` specifies what operation to invoke when the user expands the dropdown. `@lookupInput` is used to describe the JSON payload to send to that DDL operation. Any inputs defined in input.ts can be passed to the DDL operation using tripple braces. In this example we send the workspaceId field value by doing `{{{workspaceId}}}`. If your DDL operation calls an authenticated endpoint you can pass along the token used in the current operation by setting `@lookupAuthRequired true`.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
export type ExampleOperationInput = {
|
|
116
|
+
workspaceId: string;
|
|
117
|
+
/**
|
|
118
|
+
* @title user
|
|
119
|
+
* @lookupOperation list_users_ddl
|
|
120
|
+
* @lookupInput {"includePrivateChannels": false,"workspaceId": "{{{workspaceId}}}"}
|
|
121
|
+
* @lookupAuthRequired true
|
|
122
|
+
*/
|
|
123
|
+
userId: string;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Reusing types across multiple operations
|
|
128
|
+
|
|
129
|
+
You can reuse types by importing them and reuse sections of a schema by using TypeScript intersections.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// input.ts
|
|
133
|
+
import { ReusableField } from './ReusableField';
|
|
134
|
+
import { ReusableSchema } from './ReusableSchema';
|
|
135
|
+
|
|
136
|
+
export type ExampleOperationInput = SpecificSchema & ReusableSchema;
|
|
137
|
+
|
|
138
|
+
type SpecificSchema = {
|
|
139
|
+
specificField: string;
|
|
140
|
+
reusableFields: ReusableField;
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// ReusableField.ts
|
|
146
|
+
export type ReusableField = {
|
|
147
|
+
reusableFieldA: number;
|
|
148
|
+
reusableFieldB: string;
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// ReusableSchema.ts
|
|
154
|
+
export type ReusableSchema = {
|
|
155
|
+
reusableSchemaA: number;
|
|
156
|
+
reusableSchemaB: string;
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Once the imports and intersection are resolved the above example would look like this
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
export type Input = {
|
|
164
|
+
specificField: string;
|
|
165
|
+
reusableFields: {
|
|
166
|
+
reusableFieldA: number;
|
|
167
|
+
reusableFieldB: string;
|
|
168
|
+
};
|
|
169
|
+
reusableSchemaA: number;
|
|
170
|
+
reusableSchemaB: string;
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### Union types (Supporting multiple types)
|
|
175
|
+
|
|
176
|
+
If you want to support two or more different object types in your schema you can achieve this using TypeScript unions.
|
|
177
|
+
|
|
178
|
+
In the example below our input accepts an array of elements. Each element of this array can be either of type image or text. When the user adds an item to the array they will see a dropdown where they can select if this element will be an image or text. The JSdoc annotation `@title` on the image and text types will be displayed in the dropdown the user sees.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
export type ExampleOperationInput = {
|
|
182
|
+
elements: ImageOrText[];
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
type ImageOrText = Image | Text;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @title Image
|
|
189
|
+
*/
|
|
190
|
+
type Image = {
|
|
191
|
+
name: string;
|
|
192
|
+
src: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @title Text
|
|
197
|
+
*/
|
|
198
|
+
type Text = {
|
|
199
|
+
text: string;
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Required/Optional fields
|
|
204
|
+
|
|
205
|
+
By default all input fields are mandatory, you can set any to optional with a `?` in your TypeScript type. Mandatory fields get a red \* in the Tray builder UI and will warn the user that they must be filled in before attempting to run a workflow.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
export type ExampleOperationInput = {
|
|
209
|
+
mandatoryField: string;
|
|
210
|
+
optionalField?: string;
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Formatting fields
|
|
215
|
+
|
|
216
|
+
By default properties on your input type render as simple input fields. You can select a more user friendly way to render the field based on your needs by using the JSdoc annotation `@format`.
|
|
217
|
+
|
|
218
|
+
The format options available are:
|
|
219
|
+
|
|
220
|
+
- datetime - renders a date and time picker
|
|
221
|
+
- code - renders a button which on click opens a modal with a simple code editor
|
|
222
|
+
- text - for longer text inputs, expands when you enter new lines
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
export type ExampleOperationInput = {
|
|
226
|
+
/**
|
|
227
|
+
* @format datetime
|
|
228
|
+
*/
|
|
229
|
+
timestamp: string;
|
|
230
|
+
/**
|
|
231
|
+
* @format code
|
|
232
|
+
*/
|
|
233
|
+
html: string;
|
|
234
|
+
/**
|
|
235
|
+
* @format text
|
|
236
|
+
*/
|
|
237
|
+
longNotes: string;
|
|
238
|
+
};
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Default values
|
|
242
|
+
|
|
243
|
+
If you want to provide a default initial value for a field you can use the JSdoc annotation `@default`.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
export type ExampleOperationInput = {
|
|
247
|
+
/**
|
|
248
|
+
* @default string default
|
|
249
|
+
*/
|
|
250
|
+
str: string;
|
|
251
|
+
/**
|
|
252
|
+
* @default 0.1
|
|
253
|
+
*/
|
|
254
|
+
num: number;
|
|
255
|
+
/**
|
|
256
|
+
* @TJS-type integer
|
|
257
|
+
* @default 1
|
|
258
|
+
*/
|
|
259
|
+
int: number;
|
|
260
|
+
/**
|
|
261
|
+
* @default true
|
|
262
|
+
*/
|
|
263
|
+
bool: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* @default { "default": true }
|
|
266
|
+
*/
|
|
267
|
+
obj: object;
|
|
268
|
+
/**
|
|
269
|
+
* @default ["default"]
|
|
270
|
+
*/
|
|
271
|
+
arr: object[];
|
|
272
|
+
};
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Advanced fields
|
|
276
|
+
|
|
277
|
+
If you have optional fields that are for more advanced use cases you can obscure these into the Tray builder UI's advanced fields section. This can provide a cleaner interface for your operation, but can still be accessed by the user by expanding the advanced fields section. You can add fields here by using the JSdoc annotation `@advanced true`.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
export type ExampleOperationInput = {
|
|
281
|
+
normalField: string;
|
|
282
|
+
/**
|
|
283
|
+
* @advanced true
|
|
284
|
+
*/
|
|
285
|
+
advancedField?: string;
|
|
286
|
+
};
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Handler
|
|
290
|
+
|
|
291
|
+
A handler at its core, describes a function, that takes an `ctx` value with an `auth` property described by the authentication type (which is the same for all operations) and it takes an `input` value described by the input type of the operation
|
|
292
|
+
|
|
293
|
+
The output of the handler is described by the output type, in case of a success, or it could contain an error if something failed during the execution of the handler or if the third party returned an error response.
|
|
294
|
+
|
|
295
|
+
This "successful value or failure error" result of running an operation is described by the `OperationHandlerResult<T>` type, which is a sum/union/or type of `OperationHandlerSuccess<T>` and `OperationHandlerFailure`
|
|
296
|
+
|
|
297
|
+
So, the core of what an operation handler describes can be summarised as a function:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
(ctx: OperationHandlerContext<AuthType>, input: InputType) =>
|
|
301
|
+
OperationHandlerResult<OutputType>;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
However, when defining a handler, we can also specify things like validation or whether or not the handler is private, and this is where the DSL comes in.
|
|
305
|
+
|
|
306
|
+
The `handler.ts` file needs to define a handler using the `OperationHandlerSetup.configureHandler()` function, which allows for configuring all aspects of the handler by chaining function calls together for all the components of the handler.
|
|
307
|
+
|
|
308
|
+
The `OperationHandlerSetup.configureHandler()` function takes a callback that is used to configure the handler, it looks like this:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
export const myOperationHandler =
|
|
312
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
313
|
+
/* use the "handler" value to specify what implementation to use, validation, etc */
|
|
314
|
+
);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Validation
|
|
318
|
+
|
|
319
|
+
Handlers can have input validation (which runs before the handler implementation is executed to validate the input that will be used to run it) and output validation (which runs after the implementation to validate its output).
|
|
320
|
+
|
|
321
|
+
To add validation just use the `handler` argument of the callback described in the previous section:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
export const myOperationHandler = OperationHandlerSetup.configureHandler<
|
|
325
|
+
AuthType,
|
|
326
|
+
InputType,
|
|
327
|
+
OutputType
|
|
328
|
+
>((handler) =>
|
|
329
|
+
handler
|
|
330
|
+
.addInputValidation((validation) =>
|
|
331
|
+
validation
|
|
332
|
+
.condition((ctx, input) => input.id > 0)
|
|
333
|
+
.errorMessage((ctx, input) => `Id ${input.id} is not positive`)
|
|
334
|
+
)
|
|
335
|
+
.addOutputValidation((validation) =>
|
|
336
|
+
validation
|
|
337
|
+
.condition((ctx, input, output) => output.id === input.id)
|
|
338
|
+
.errorMessage(
|
|
339
|
+
(ctx, input, output) => `Output and Input ids don't match`
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Note that validation is optional, the only thing that is necessary to define a handler is its implementation.
|
|
346
|
+
|
|
347
|
+
## Implementation
|
|
348
|
+
|
|
349
|
+
The main aspect of a handler is its implementation, which can be `HTTP` if the operation will make an HTTP call to a third party, or `Composite` if the operation will combine zero or more operations when it runs, more implementations for other protocols will be added in the future.
|
|
350
|
+
|
|
351
|
+
A handler can only have one implementation, it describes what the handler does when it receives a request.
|
|
352
|
+
|
|
353
|
+
## HTTP Implementation
|
|
354
|
+
|
|
355
|
+
A very simple handler that makes an HTTP call can be configured in the following way:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
export const myOperationHandler =
|
|
359
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
360
|
+
handler.addInputValidation(...)
|
|
361
|
+
.addOutputValidation(...)
|
|
362
|
+
.usingHttp((http) =>
|
|
363
|
+
http.get('https://someapi.com/someresource/:id')
|
|
364
|
+
.handleRequest((ctx, input, request) =>
|
|
365
|
+
request.addPathParameter('id', input.id.toString()).withoutBody()
|
|
366
|
+
)
|
|
367
|
+
.handleResponse((ctx, input, response) => response.parseWithBodyAsJson())
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The previous handler makes a `GET` http request, which is defined by the `http.get()` call, after which a `handleRequest()` function is chained, whose purpose is to take an auth value, an input value and a request configuration and add the necessary arguments to that request configuration based on what we want the http call to have, the supported methods on the request configuration are:
|
|
373
|
+
|
|
374
|
+
- `addPathParameter(name, value)`: Will replace a parameter on the path specified as `:name` in the url as shown in the previous example, the value will be url encoded
|
|
375
|
+
- `addHeader(name, value)`: Adds a header to the request
|
|
376
|
+
- `withBearerToken(token)`: Adds an `Authorization` header with a `Bearer` token
|
|
377
|
+
- `addQueryString(name, value)`: Adds a query string to the request, the value will be url encoded
|
|
378
|
+
- `withBodyAsJson(body)`: Adds a body to the request that will be sent as json.
|
|
379
|
+
- `withBodyAsText(body)`: Adds a body to the request that will be sent as a plain text.
|
|
380
|
+
- `withBodyAsMultipart(body)`: Accepts a specific object as body which is then sent as a multipart/form-data body in the request. The `body` input for `withBodyAsMultipart` has the following type:
|
|
381
|
+
```typescript
|
|
382
|
+
type MultipartBody = {
|
|
383
|
+
fields: Record<string, string>;
|
|
384
|
+
files: Record<string, FileReference>;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- `withBodyAsFile(FileReference)`: Adds the source file to the request body as binary that will sent to the target server. The FileReference input has the following type:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
type FileReference = {
|
|
392
|
+
name: string;
|
|
393
|
+
url: string;
|
|
394
|
+
mime_type: string;
|
|
395
|
+
expires: number;
|
|
396
|
+
};
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- `withoutBody()`: Sends a empty body in the request.
|
|
400
|
+
|
|
401
|
+
A handler with an authenticated POST request would look like this:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
export const myOperationHandler =
|
|
405
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
406
|
+
handler.addInputValidation(...)
|
|
407
|
+
.addOutputValidation(...)
|
|
408
|
+
.usingHttp((http) =>
|
|
409
|
+
http.post('https://someapi.com/someresource')
|
|
410
|
+
.handleRequest((ctx, input, request) =>
|
|
411
|
+
request.withBearerToken(auth.access_token)
|
|
412
|
+
.withBodyAsJson(input)
|
|
413
|
+
)
|
|
414
|
+
.handleResponse((ctx, input, response) => response.parseWithBodyAsJson())
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The input does not have to match what is sent as the body, if for example the input has other flags that specify how the connector needs to behave and only part of it contains the body, the `withBodyAsJson` method can be called in the following way:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
request.withBodyAsJson({ name: input.name, title: input.title });
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
So the `handlerRequest` function can transform the input in any way it needs to before sending the HTTP request and the same is true for the `handleResponse`, in the previous examples, the `handleResponse` simply read the response body as json and returned it, but it can be more complex if necessary.
|
|
426
|
+
|
|
427
|
+
Supported methods on the response configuration are:
|
|
428
|
+
|
|
429
|
+
- `parseWithBodyAsJson()`: Returns a the requests response as a JSON
|
|
430
|
+
- `parseWithBodyAsText((text) => { //logic to transform it to match your output.})`: Returns a requests response as a text to the callback and it can be manipulated to match the operations output schema.
|
|
431
|
+
|
|
432
|
+
Just like with the input type and the request, the type of the json body in the response can be different from the output type.
|
|
433
|
+
|
|
434
|
+
This is an example of a handler that transforms the response body into the output type:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
export const myOperationHandler =
|
|
438
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
439
|
+
handler.addInputValidation(...)
|
|
440
|
+
.addOutputValidation(...)
|
|
441
|
+
.usingHttp((http) =>
|
|
442
|
+
http.post('https://someapi.com/someresource')
|
|
443
|
+
.handleRequest(...)
|
|
444
|
+
.handleResponse((response) => {
|
|
445
|
+
const httpResponseBody = response.parseWithBodyAsJson<{message: string}>()
|
|
446
|
+
if (httpResponseBody.isSuccess) {
|
|
447
|
+
const originalMessage = httpResponseBody.value.message
|
|
448
|
+
const extendedMessage = originalMessage + ' Extension'
|
|
449
|
+
return OperationHandlerResult.success({ message: extendedMessage })
|
|
450
|
+
}
|
|
451
|
+
return httpResponseBody
|
|
452
|
+
})
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Instead of using an `if` in the previous case, there is also a `OperationHandlerResult.map` function that can do the same with a callback.
|
|
458
|
+
|
|
459
|
+
The handler can also return successful responses for some failure cases or viceversa, this is an example of a handler that "recovers" from errors to always return a successful response:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
export const myOperationHandler =
|
|
463
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
464
|
+
handler.addInputValidation(...)
|
|
465
|
+
.addOutputValidation(...)
|
|
466
|
+
.usingHttp((http) =>
|
|
467
|
+
http.post('https://someapi.com/someresource')
|
|
468
|
+
.handleRequest(...)
|
|
469
|
+
.handleResponse((response) => {
|
|
470
|
+
const httpResponseBody = response.withBodyAsJson()
|
|
471
|
+
if (httpResponseBody.isFailure) {
|
|
472
|
+
return OperationHandlerResult.success({ completed: false })
|
|
473
|
+
}
|
|
474
|
+
return OperationHandlerResult.success({ completed: true })
|
|
475
|
+
})
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
An example handler for sending multipart/form-data
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
export const myOperationHandler =
|
|
484
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
485
|
+
handler.addInputValidation(...)
|
|
486
|
+
.addOutputValidation(...)
|
|
487
|
+
.usingHttp((http) =>
|
|
488
|
+
http.post('https://someapi.com/someresource')
|
|
489
|
+
.handleRequest((ctx, input, request) =>
|
|
490
|
+
request.withBodyAsMultipart({
|
|
491
|
+
fields: {
|
|
492
|
+
field1: 'Hello World!'
|
|
493
|
+
},
|
|
494
|
+
files: {
|
|
495
|
+
file1: input.file
|
|
496
|
+
}
|
|
497
|
+
}) // where `file` is of type FileReference.
|
|
498
|
+
)
|
|
499
|
+
.handleResponse((ctx, input, response) =>
|
|
500
|
+
response.parseWithBodyAsJson()
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
);
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
An example handler that can upload a file to a server
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
export const myOperationHandler =
|
|
510
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
511
|
+
handler.addInputValidation(...)
|
|
512
|
+
.addOutputValidation(...)
|
|
513
|
+
.usingHttp((http) =>
|
|
514
|
+
http.post('https://someapi.com/someresource')
|
|
515
|
+
.handleRequest((ctx, input, request) =>
|
|
516
|
+
request.withBodyAsFile(input.file) // where file is of type FileReference.
|
|
517
|
+
)
|
|
518
|
+
.handleResponse((ctx, input, response) =>
|
|
519
|
+
response.parseWithBodyAsJson()
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Composite Implementation
|
|
526
|
+
|
|
527
|
+
Composite handlers are used to define behaviours by invoking zero or more operations as part of their behaviour.
|
|
528
|
+
|
|
529
|
+
They can be used to write "helper" connectors (such as those in Tray's builder), DDL operations or complex operations
|
|
530
|
+
like an "upsert" that combines more granular "read, create and update" operations.
|
|
531
|
+
|
|
532
|
+
A very simple composite handler, that just concatenates the `firstName` and `lastName` arguments it gets from the input into one string:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
export const myOperationHandler =
|
|
536
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
537
|
+
handler.addInputValidation(...)
|
|
538
|
+
.addOutputValidation(...)
|
|
539
|
+
.usingComposite(async (ctx, input, invoke) => {
|
|
540
|
+
const fullName = input.firstName + ' ' + input.lastName
|
|
541
|
+
return OperationHandlerResult.success({fullName: fullName})
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
As an example of more complex behaviour, this handler reads a list of products using another operation and converts the result into a simple list of text and value pairs, this is known as a Dynamic Data List (DDL) operation used to help users select values as part of configuring workflows within the tray builder.
|
|
547
|
+
|
|
548
|
+
To do this, the handler needs to invoke the regular `getProducts` operation, this is accomplished by using the `invoke` functions that composite handlers have access to, and passing it a **handler reference** and an input (no need to pass the auth value as it will be passed automatically), the handler reference passed to the `invoke` function is the result of the `OperationHandlerSetup.configureHandler()` function, which is why they are saved into a constant and exported like this:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
export const getProductsHandler =
|
|
552
|
+
OperationHandlerSetup.configureHandler<AuthType, GetProductsInput, GetProductsOutput>((handler) =>
|
|
553
|
+
handler.usingHttp(...)
|
|
554
|
+
);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The `getProductsHandler` constant contains the handler reference, which also has the input and output type information as part of its type, to make sure that when invoked or tested, only valid input and output values can be used.
|
|
558
|
+
|
|
559
|
+
To invoke the handler it is a simple call to the `invoke` function that takes the handler reference as an argument, and returns another function that takes the input type of that handler and returns a `Promise<OperationHandlerResult<T>>` where `T` is the output type of the handler and `OperationHandlerResult` contains a failure response if the call failed or a success response with a value of type `T` if the call was successful:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
const productsResult: OperationHandlerResult<GetProductsOutput> = await invoke(
|
|
563
|
+
getProductsHandler
|
|
564
|
+
)({ storeId: input.storeId });
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
The output type of a DDL operation is defined by the `DDLOperationOutput<T>` type, where `T` is the type of the values and can be a string or a number, that type has one field called `results` which is an array of objects of type `{text: string, value: T}`.
|
|
568
|
+
|
|
569
|
+
To use it as the output, it is recommended to define a custom type for the operation as usual in `output.ts` that derives from `DDLOperationOutput<T>` specifying whether `T` is of type string or number, like this example `output.ts` file:
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
import { DDLOperationOutput } from '@trayio/cdk-dsl/connector/operation/OperationHandler';
|
|
573
|
+
|
|
574
|
+
export type GetProductListDdlOutput = DDLOperationOutput<number>; //the values of the elements of the DDL are of type number
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
With that in mind, this is what the DDL handler would look like:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
export const getProductsDDLHandler = OperationHandlerSetup.configureHandler<
|
|
581
|
+
AuthType,
|
|
582
|
+
GetProductsDDLInput,
|
|
583
|
+
GetProductsDDLOutput
|
|
584
|
+
>((handler) =>
|
|
585
|
+
handler.usingComposite(async (auth, input, invoke) => {
|
|
586
|
+
//invoke the products operation
|
|
587
|
+
const productsResult: OperationHandlerResult<GetProductsOutput> =
|
|
588
|
+
await invoke(getProductsHandler)({ storeId: input.storeId });
|
|
589
|
+
|
|
590
|
+
//if the invocation failed, propagate the failure
|
|
591
|
+
if (productsResult.isFailure) {
|
|
592
|
+
return productsResult;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
//productsResult is of type `OperationHandlerSuccess` now, because we handled the failure case above, so we can get the value
|
|
596
|
+
const products = productsResult.value;
|
|
597
|
+
|
|
598
|
+
//converts a product list into a list of text (label) and values (identifiers) for the DDL
|
|
599
|
+
const productsDDL = products.map((product) => {
|
|
600
|
+
return {
|
|
601
|
+
text: product.title,
|
|
602
|
+
value: product.id,
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
//returns the DDL list in an object with a "result" value to match the GetProductsDDLOutput type
|
|
607
|
+
return OperationHandlerResult.success({
|
|
608
|
+
result: productsDDL,
|
|
609
|
+
});
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
There are several things to note about the handler, both the DDL handler and the `getProductsHandler` expect a `storeId` in the input to return a list of products for that store.
|
|
615
|
+
|
|
616
|
+
The `getProductsHandler` is invoked using the `invoke` function that composite handlers receive as an argument, passing the handler reference, and then calling the result as a function passing the input that handler expects, in this case, just an object with a `storeId`
|
|
617
|
+
|
|
618
|
+
The result of that invocation is of type `Promise<OperationHandlerResult<GetProductsOutput>>`, that is, a promise that has a value that is described by the invoked handler's output type, which is wrapped in the result object because it could be a successful invocation or a failure as described in previous sections.
|
|
619
|
+
|
|
620
|
+
The `await` keyword unwraps the promise, and we are left with a `OperationHandlerResult<GetProductsOutput>`, which forces the handler to deal with both the failure case as well as the success case.
|
|
621
|
+
|
|
622
|
+
There are multiple ways to do this
|
|
623
|
+
|
|
624
|
+
- Using `if` or `switch` statements to narrow down the type as shown in the example
|
|
625
|
+
- Using the `OperationHandlerResult.getSuccessfulValueOrFail()` function which unwraps the value if successful or terminates the function propagating the error if it is not
|
|
626
|
+
- Using the `OperationHandlerResult.map()` function, which takes an `OperationHandlerResult<T>` as an argument and a function to convert `T` to another type in case it is successful, propagating the failure if it is not (works in the same way it works for the map function in `Array<T>`)
|
|
627
|
+
|
|
628
|
+
Once the handler has access to the product list value, it just needs to convert each element to a `{text: string, value: number}` pair and return that list in an object with a `result` property as shown above.
|
|
629
|
+
|
|
630
|
+
Finally, for DDL operations, we need to add an extra `type` property in the `operation.json` file with `ddl` as the value:
|
|
631
|
+
|
|
632
|
+
```json
|
|
633
|
+
{
|
|
634
|
+
"name": "get_products_ddl",
|
|
635
|
+
"title": "Get Products DDL",
|
|
636
|
+
"description": "Returns a DDL with product ids as values and product titles as labels",
|
|
637
|
+
"type": "ddl"
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
This will categorise this operation as a DDL and will exclude it from the list of visible operations of the connector.
|
|
642
|
+
|
|
643
|
+
## Testing
|
|
644
|
+
|
|
645
|
+
The CDK DSL has declarative testing functions to test a handler's behaviour, the tests are in the `handler.test.ts` file within the operation folder, which can have zero, one or many test cases for that given operation.
|
|
646
|
+
|
|
647
|
+
The `OperationHandlerTestSetup.configureHandlerTest()` function is used to describe a test, it takes a handler reference and a callback with an object used to configure the test, in a similar way handlers are configured.
|
|
648
|
+
|
|
649
|
+
This is what a very basic test looks like:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
OperationHandlerTestSetup.configureHandlerTest(
|
|
653
|
+
myOperationHandler,
|
|
654
|
+
(handlerTest) =>
|
|
655
|
+
handlerTest
|
|
656
|
+
.usingHandlerContext('test') //will use `test.ctx.json` as the context value which includes authentication values for all test cases
|
|
657
|
+
.nothingBeforeAll()
|
|
658
|
+
.testCase('should do something', (testCase) =>
|
|
659
|
+
testCase
|
|
660
|
+
.usingHandlerContext('another') //optionally, a test case can define its own context instead of using the default one defined for all tests
|
|
661
|
+
.givenNothing()
|
|
662
|
+
.when(() => /* return an input that matches the input type */)
|
|
663
|
+
.then(({ output }) => {
|
|
664
|
+
/* output is OperationHandlerResult<T> where T is a value matching the output type */
|
|
665
|
+
|
|
666
|
+
//This will contain a value of type T if the operation was successful or the test will fail if not
|
|
667
|
+
const successValue = OperationHandlerResult.getSuccessfulValueOrFail(output)
|
|
668
|
+
|
|
669
|
+
// jest-style matchers like "expect" are available here
|
|
670
|
+
expect(successValue).toEqual(...)
|
|
671
|
+
})
|
|
672
|
+
.finallyDoNothing()
|
|
673
|
+
)
|
|
674
|
+
.nothingAfterAll()
|
|
675
|
+
);
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
The structure of a test is well defined, and the type safe declarative DSL will enforce that, in particular, there are a number of aspects that would apply to all test cases:
|
|
679
|
+
|
|
680
|
+
- A default context to use for all test cases
|
|
681
|
+
- Run one or more operations (doesn't have to be the one being tested) before all test cases using the `beforeAll()` function, or don't do anything before all test cases using the `nothingBeforeAll()` function, these functions can only be used before adding test cases (this is enforced by the type system)
|
|
682
|
+
- Add a test case using the `testCase()` function.
|
|
683
|
+
- Run one or more operations (doesn't have to be the one being tested) after all test cases using the `afterAll()` function, or don't do anything after all test cases using the `nothingAfterAll()` function, these functions can only be used after defining test cases, and no test cases can be added after this (this is enforced by the type system)
|
|
684
|
+
|
|
685
|
+
As for the test cases, they use a BDD style Given/When/Then convention, in particular a test case has:
|
|
686
|
+
|
|
687
|
+
- An optional `usingHandlerContext()` function at the beginning of the test case to use a different context than the default for all test cases
|
|
688
|
+
- A `given()` function to run one or more operations at the beginning of the test case or `givenNothing()` to go straight to running the operation under test
|
|
689
|
+
- A `when()` function to create an input value that will be used to run the operation under test, that value needs to match the input type of the operation
|
|
690
|
+
- A `then()` function that gets the output, input, auth and optionally the result of `beforeAll()` and `given()` if present, which can be used to do the assertions of the test case using jest-style matchers
|
|
691
|
+
- A `finally()` function to run one or more operations at the end of the test case, usually for cleanup, or `finallyDoNothing()` to don't do anything else after the assertions.
|
|
692
|
+
|
|
693
|
+
Both the `beforeAll()` and `given()` functions allow to run multiple operations before all test cases or before a single test case, they receive the `invoke` function as an argument just like composite handlers and they return an object that can contain some of relevant information about the operations that ran if necessary (like ids of things created) in a value that can be accessed by the `when()`, `then()`, `finally()` and `afterAll()` functions, they receive them as arguments.
|
|
694
|
+
|
|
695
|
+
As an example, the following test is for an `updateProduct` operation, it creates a store for all test cases, and a product for every test case to test the update on, using `beforeAll` and `given` respectively, and accessing the identities of the created objects from the `testContext` (the output of `beforeAll`) and the `testCaseContext` (the output of `given`):
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
OperationHandlerTestSetup.configureHandlerTest(
|
|
699
|
+
updateProductOperationHandler,
|
|
700
|
+
(handlerTest) =>
|
|
701
|
+
handlerTest
|
|
702
|
+
.usingHandlerContext('test')
|
|
703
|
+
.beforeAll<{ storeId: string }>(async (auth, invoke) => {
|
|
704
|
+
//Creates an store that will be used by all tests
|
|
705
|
+
const createdStoreResult = invoke(createStoreHandler)({
|
|
706
|
+
name: 'something',
|
|
707
|
+
});
|
|
708
|
+
return OperationHandlerResult.map(
|
|
709
|
+
createdStoreResult,
|
|
710
|
+
(createdStoreOutput) => {
|
|
711
|
+
storeId: createdStoreOutput.id;
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
})
|
|
715
|
+
.testCase('should do something', (testCase) =>
|
|
716
|
+
testCase
|
|
717
|
+
.given<{ productId: string }>((auth, testContext, invoke) => {
|
|
718
|
+
//Creates an product in the store to be used by the test
|
|
719
|
+
const createdProductResult = invoke(createProductHandler)({
|
|
720
|
+
name: 'some product',
|
|
721
|
+
storeId: testContext.storeId,
|
|
722
|
+
});
|
|
723
|
+
return OperationHandlerResult.map(
|
|
724
|
+
createdProductResult,
|
|
725
|
+
(createdProductOutput) => {
|
|
726
|
+
productId: createdProductOutput.id;
|
|
727
|
+
}
|
|
728
|
+
);
|
|
729
|
+
})
|
|
730
|
+
.when((auth, testContext, testCaseContext) => ({
|
|
731
|
+
productId: testCaseContext.productId,
|
|
732
|
+
name: 'updated name',
|
|
733
|
+
}))
|
|
734
|
+
.then(({ output }) => {
|
|
735
|
+
const outputValue =
|
|
736
|
+
OperationHandlerResult.getSuccessfulValueOrFail(output);
|
|
737
|
+
expect(outputValue.name).toEqual('updated name');
|
|
738
|
+
})
|
|
739
|
+
.finally(({ testCaseContext }) => {
|
|
740
|
+
//Deletes the product created for the test
|
|
741
|
+
return invoke(deleteProductHandler)({
|
|
742
|
+
productId: testCaseContext.productId,
|
|
743
|
+
});
|
|
744
|
+
})
|
|
745
|
+
)
|
|
746
|
+
.afterAll(({ testContext }) => {
|
|
747
|
+
//Deletes the store created for all test cases
|
|
748
|
+
return invoke(deleteStoreHandler)({ storeId: testContext.storeId });
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
It is worth noting that while the DSL can be used to write complex functional tests, in practice, a connector test's focus is more about making sure that operations are properly communicating with the underlying implementation instead of testing its functionality, but ultimately it is up to the developer to decide how much and what type of coverage suits a given connector best.
|
|
754
|
+
|
|
755
|
+
## File Handling.
|
|
756
|
+
|
|
757
|
+
### An example operation for a file download.
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// input.ts
|
|
761
|
+
export type DownloadFileInput = {}; // add input params as needed.
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
// output.ts
|
|
766
|
+
import { FileReference } from '@trayio/cdk-dsl/dist/connector/operation/OperationHandler';
|
|
767
|
+
|
|
768
|
+
export type DownloadFileOutput = {
|
|
769
|
+
file: FileReference;
|
|
770
|
+
};
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
// handler.ts
|
|
775
|
+
handler.usingHttp((http) =>
|
|
776
|
+
http
|
|
777
|
+
.get('https://someapi.com/someresource')
|
|
778
|
+
.handleRequest((ctx, input, request) => request.withoutBody())
|
|
779
|
+
.handleResponse((ctx, input, response) =>
|
|
780
|
+
response.parseWithBodyAsFile<DownloadFileOutput['file']>((file) =>
|
|
781
|
+
OperationHandlerResult.success({ file })
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
);
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### An example operation for a file upload.
|
|
788
|
+
|
|
789
|
+
_(Note: This upload operation is not applicable for multipart/form-data endpoints; see section below for multipart/form-data)_
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
// input.ts
|
|
793
|
+
import { FileReference } from '@trayio/cdk-dsl/dist/connector/operation/OperationHandler';
|
|
794
|
+
|
|
795
|
+
export type UploadFileInput = {
|
|
796
|
+
file: FileReference;
|
|
797
|
+
};
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
// output.ts
|
|
802
|
+
export type UploadFileOutput = {
|
|
803
|
+
id: number; // response from the API.
|
|
804
|
+
};
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
// handler.ts
|
|
809
|
+
handler.usingHttp((http) =>
|
|
810
|
+
http
|
|
811
|
+
.post('https://someapi.com/someresource')
|
|
812
|
+
.handleRequest((ctx, input, request) =>
|
|
813
|
+
request.withBodyAsFile(input.file)
|
|
814
|
+
)
|
|
815
|
+
.handleResponse((ctx, input, response) => response.parseWithBodyAsJson())
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
### An example operation sending files in multipart/form-data
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
// input.ts
|
|
823
|
+
import { FileReference } from '@trayio/cdk-dsl/dist/connector/operation/OperationHandler';
|
|
824
|
+
|
|
825
|
+
export type UploadFileInput = {
|
|
826
|
+
files: FileReference[];
|
|
827
|
+
};
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// output.ts
|
|
832
|
+
export type UploadFileOutput = {
|
|
833
|
+
success: boolean; // response from the API.
|
|
834
|
+
};
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
export const myOperationHandler =
|
|
839
|
+
OperationHandlerSetup.configureHandler<AuthType, InputType, OutputType>((handler) =>
|
|
840
|
+
handler.addInputValidation(...)
|
|
841
|
+
.addOutputValidation(...)
|
|
842
|
+
.usingHttp((http) =>
|
|
843
|
+
http.post('https://someapi.com/someresource')
|
|
844
|
+
.handleRequest((ctx, input, request) =>
|
|
845
|
+
request.withBodyAsMultipart({
|
|
846
|
+
fields: {},
|
|
847
|
+
files: { // where `files` is an array of FileReference.
|
|
848
|
+
file1: input.files[0],
|
|
849
|
+
file2: input.files[1],
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
)
|
|
853
|
+
.handleResponse((ctx, input, response) =>
|
|
854
|
+
response.parseWithBodyAsJson()
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
```
|