@superblocksteam/sdk-api 2.0.164 → 2.0.166-next.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/README.md +1 -2
- package/dist/errors.d.ts +36 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +244 -8
- package/dist/errors.js.map +1 -1
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +189 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.js +2 -5
- package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
- package/dist/integrations/base/index.d.ts +2 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/index.js +1 -0
- package/dist/integrations/base/index.js.map +1 -1
- package/dist/integrations/base/request-body.d.ts +29 -0
- package/dist/integrations/base/request-body.d.ts.map +1 -0
- package/dist/integrations/base/request-body.js +238 -0
- package/dist/integrations/base/request-body.js.map +1 -0
- package/dist/integrations/base/request-body.test.d.ts +2 -0
- package/dist/integrations/base/request-body.test.d.ts.map +1 -0
- package/dist/integrations/base/request-body.test.js +466 -0
- package/dist/integrations/base/request-body.test.js.map +1 -0
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +15 -14
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts +2 -2
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +5 -6
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +55 -11
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +7 -0
- package/dist/integrations/base/types.js.map +1 -1
- package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
- package/dist/integrations/cosmosdb/client.js +2 -5
- package/dist/integrations/cosmosdb/client.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +106 -1
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/dynamodb/client.d.ts.map +1 -1
- package/dist/integrations/dynamodb/client.js +2 -5
- package/dist/integrations/dynamodb/client.js.map +1 -1
- package/dist/integrations/gcs/client.d.ts.map +1 -1
- package/dist/integrations/gcs/client.js +2 -5
- package/dist/integrations/gcs/client.js.map +1 -1
- package/dist/integrations/gsheets/client.d.ts.map +1 -1
- package/dist/integrations/gsheets/client.js +2 -5
- package/dist/integrations/gsheets/client.js.map +1 -1
- package/dist/integrations/mongodb/client.d.ts.map +1 -1
- package/dist/integrations/mongodb/client.js +2 -5
- package/dist/integrations/mongodb/client.js.map +1 -1
- package/dist/integrations/restapiintegration/client.body-types.test.d.ts +2 -0
- package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +1 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js +227 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -0
- package/dist/integrations/restapiintegration/client.test.js +56 -30
- package/dist/integrations/restapiintegration/client.test.js.map +1 -1
- package/dist/integrations/s3/client.d.ts.map +1 -1
- package/dist/integrations/s3/client.js +2 -5
- package/dist/integrations/s3/client.js.map +1 -1
- package/dist/integrations/salesforce/client.d.ts.map +1 -1
- package/dist/integrations/salesforce/client.js +2 -5
- package/dist/integrations/salesforce/client.js.map +1 -1
- package/dist/integrations/slack/client.d.ts.map +1 -1
- package/dist/integrations/slack/client.js +4 -13
- package/dist/integrations/slack/client.js.map +1 -1
- package/dist/integrations/slack/client.test.js +20 -0
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/dist/integrations/utils.d.ts +8 -3
- package/dist/integrations/utils.d.ts.map +1 -1
- package/dist/integrations/utils.js +42 -1
- package/dist/integrations/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.test.ts +257 -0
- package/src/errors.ts +304 -10
- package/src/integrations/base/decode-worker-binary-response.ts +5 -7
- package/src/integrations/base/index.ts +3 -0
- package/src/integrations/base/request-body.test.ts +565 -0
- package/src/integrations/base/request-body.ts +392 -0
- package/src/integrations/base/rest-api-client-base.ts +24 -19
- package/src/integrations/base/rest-api-integration-client.ts +14 -10
- package/src/integrations/base/types.ts +66 -11
- package/src/integrations/box/README.md +4 -60
- package/src/integrations/cosmosdb/client.ts +8 -7
- package/src/integrations/documentation-resolver.test.ts +130 -1
- package/src/integrations/dropbox/README.md +1 -71
- package/src/integrations/dropbox/docs.manifest.json +10 -1
- package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
- package/src/integrations/dropbox/overlays/upload.md +89 -0
- package/src/integrations/dynamodb/client.ts +8 -7
- package/src/integrations/elasticsearch/README.md +0 -80
- package/src/integrations/elasticsearch/docs.manifest.json +10 -1
- package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
- package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
- package/src/integrations/gcs/client.ts +8 -7
- package/src/integrations/googledrive/README.md +10 -36
- package/src/integrations/gsheets/client.ts +8 -7
- package/src/integrations/jira/README.md +0 -26
- package/src/integrations/jira/docs.manifest.json +10 -1
- package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
- package/src/integrations/jira/overlays/attachments.md +30 -0
- package/src/integrations/lakebase/README.md +1 -3
- package/src/integrations/mongodb/client.ts +8 -7
- package/src/integrations/postgres/README.md +1 -3
- package/src/integrations/restapiintegration/client.body-types.test.ts +323 -0
- package/src/integrations/restapiintegration/client.test.ts +68 -32
- package/src/integrations/restapiintegration/docs.manifest.json +8 -0
- package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +7 -0
- package/src/integrations/restapiintegration/overlays/request-body-types.md +115 -0
- package/src/integrations/restapiintegration/overlays/response-types-binary.md +4 -8
- package/src/integrations/s3/client.ts +8 -7
- package/src/integrations/salesforce/client.ts +9 -7
- package/src/integrations/slack/client.test.ts +30 -0
- package/src/integrations/slack/client.ts +13 -19
- package/src/integrations/snowflakepostgres/README.md +1 -3
- package/src/integrations/stabilityai/README.md +0 -87
- package/src/integrations/stabilityai/docs.manifest.json +10 -1
- package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
- package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
- package/src/integrations/stripe/README.md +0 -201
- package/src/integrations/stripe/docs.manifest.json +10 -1
- package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
- package/src/integrations/stripe/overlays/request-bodies.md +204 -0
- package/src/integrations/utils.ts +51 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
## Bulk Operations
|
|
2
|
+
|
|
3
|
+
`/_bulk` expects newline-delimited JSON, so pass `bodyType: "raw"` with the pre-built NDJSON string and set `Content-Type: application/x-ndjson`. Raw mode adds no headers of its own. Without `bodyType`, `apiRequest()` would JSON-encode the string a second time and Elasticsearch would reject it.
|
|
4
|
+
|
|
5
|
+
Bulk operations require the request bodies to be newline-delimited JSON, not a JSON array:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// WRONG - Regular JSON array
|
|
9
|
+
const body = [{ index: { _index: "products" } }, { name: "Product" }];
|
|
10
|
+
|
|
11
|
+
// CORRECT - Newline-delimited JSON
|
|
12
|
+
const body = '{"index":{"_index":"products"}}\n{"name":"Product"}\n';
|
|
13
|
+
|
|
14
|
+
// Or build programmatically
|
|
15
|
+
const operations = [{ index: { _index: "products" } }, { name: "Product" }];
|
|
16
|
+
const body = operations.map(JSON.stringify).join("\n") + "\n";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
const BulkResponseSchema = z.object({
|
|
21
|
+
took: z.number(),
|
|
22
|
+
errors: z.boolean(),
|
|
23
|
+
items: z.array(
|
|
24
|
+
z.object({
|
|
25
|
+
index: z
|
|
26
|
+
.object({
|
|
27
|
+
_index: z.string(),
|
|
28
|
+
_id: z.string(),
|
|
29
|
+
status: z.number(),
|
|
30
|
+
result: z.string().optional(),
|
|
31
|
+
error: z
|
|
32
|
+
.object({
|
|
33
|
+
type: z.string(),
|
|
34
|
+
reason: z.string(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
delete: z
|
|
40
|
+
.object({
|
|
41
|
+
_index: z.string(),
|
|
42
|
+
_id: z.string(),
|
|
43
|
+
status: z.number(),
|
|
44
|
+
result: z.string().optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Bulk index
|
|
52
|
+
const result = await ctx.integrations.es.apiRequest(
|
|
53
|
+
{
|
|
54
|
+
method: "POST",
|
|
55
|
+
path: "/_bulk",
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/x-ndjson",
|
|
58
|
+
},
|
|
59
|
+
bodyType: "raw",
|
|
60
|
+
body:
|
|
61
|
+
[
|
|
62
|
+
{ index: { _index: "products", _id: "1" } },
|
|
63
|
+
{ name: "Product 1", price: 10 },
|
|
64
|
+
{ index: { _index: "products", _id: "2" } },
|
|
65
|
+
{ name: "Product 2", price: 20 },
|
|
66
|
+
]
|
|
67
|
+
.map(JSON.stringify)
|
|
68
|
+
.join("\n") + "\n",
|
|
69
|
+
},
|
|
70
|
+
{ response: BulkResponseSchema },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (result.errors) {
|
|
74
|
+
result.items.forEach((item) => {
|
|
75
|
+
if (item.index?.error) {
|
|
76
|
+
console.error(`Failed to index: ${item.index.error.reason}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
```
|
|
@@ -9,7 +9,10 @@ import type { z } from "zod";
|
|
|
9
9
|
|
|
10
10
|
import type { Plugin as GCSPlugin } from "@superblocksteam/types/dist/src/plugins/gcs/v1/plugin_pb";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
RestApiValidationError,
|
|
14
|
+
restApiValidationErrorFromZodError,
|
|
15
|
+
} from "../../errors.js";
|
|
13
16
|
import { IntegrationError } from "../../runtime/errors.js";
|
|
14
17
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
15
18
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
@@ -71,12 +74,10 @@ export class GCSClientImpl implements GCSClient, IntegrationClientImpl {
|
|
|
71
74
|
const parseResult = schema.safeParse(result);
|
|
72
75
|
|
|
73
76
|
if (!parseResult.success) {
|
|
74
|
-
throw
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
data: result,
|
|
79
|
-
},
|
|
77
|
+
throw restApiValidationErrorFromZodError(
|
|
78
|
+
"Result validation failed",
|
|
79
|
+
parseResult.error,
|
|
80
|
+
result,
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Google Drive Client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Download and manage files in Google Drive. File content uploads are not available through this integration; see [File Content Uploads Are Not Available](#file-content-uploads-are-not-available).
|
|
4
4
|
|
|
5
5
|
## Methods
|
|
6
6
|
|
|
@@ -162,32 +162,6 @@ const folder = await ctx.integrations.drive.apiRequest(
|
|
|
162
162
|
console.log(`Created folder: ${folder.id}`);
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
### Upload a File
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// For simple uploads (< 5MB)
|
|
169
|
-
const uploadedFile = await ctx.integrations.drive.apiRequest(
|
|
170
|
-
{
|
|
171
|
-
method: "POST",
|
|
172
|
-
path: "/upload/drive/v3/files",
|
|
173
|
-
params: {
|
|
174
|
-
uploadType: "multipart",
|
|
175
|
-
},
|
|
176
|
-
body: {
|
|
177
|
-
metadata: {
|
|
178
|
-
name: "report.txt",
|
|
179
|
-
parents: [folderId],
|
|
180
|
-
},
|
|
181
|
-
media: {
|
|
182
|
-
mimeType: "text/plain",
|
|
183
|
-
body: "File content here",
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
{ response: FileSchema },
|
|
188
|
-
);
|
|
189
|
-
```
|
|
190
|
-
|
|
191
165
|
### Update File Metadata
|
|
192
166
|
|
|
193
167
|
```typescript
|
|
@@ -300,7 +274,7 @@ All methods accept an optional `metadata` parameter as the last argument for dia
|
|
|
300
274
|
```typescript
|
|
301
275
|
// WRONG - These methods do not exist
|
|
302
276
|
await drive.listFiles({ ... });
|
|
303
|
-
await drive.
|
|
277
|
+
await drive.createFolder({ ... });
|
|
304
278
|
|
|
305
279
|
// CORRECT - Use apiRequest
|
|
306
280
|
await ctx.integrations.drive.apiRequest(
|
|
@@ -394,21 +368,22 @@ const sizeBytes = parseInt(file.size, 10);
|
|
|
394
368
|
const sizeMB = sizeBytes / (1024 * 1024);
|
|
395
369
|
```
|
|
396
370
|
|
|
397
|
-
###
|
|
371
|
+
### Base URL Prefix
|
|
398
372
|
|
|
399
|
-
The base URL already includes `/drive/v3`, so regular API paths omit this prefix
|
|
373
|
+
The base URL already includes `/drive/v3`, so regular API paths omit this prefix:
|
|
400
374
|
|
|
401
375
|
```typescript
|
|
402
|
-
//
|
|
376
|
+
// CORRECT - base URL already has /drive/v3
|
|
403
377
|
const path = "/files";
|
|
404
378
|
|
|
405
|
-
//
|
|
406
|
-
const path = "/upload/drive/v3/files";
|
|
407
|
-
|
|
408
|
-
// WRONG - Duplicates base URL prefix for regular calls
|
|
379
|
+
// WRONG - duplicates the base URL prefix
|
|
409
380
|
const path = "/drive/v3/files";
|
|
410
381
|
```
|
|
411
382
|
|
|
383
|
+
### File Content Uploads Are Not Available
|
|
384
|
+
|
|
385
|
+
Google Drive serves every content upload endpoint (`uploadType=media`, `multipart`, and `resumable`) from `https://www.googleapis.com/upload/drive/v3`. This integration's base URL is fixed by the Google Drive integration template to `https://www.googleapis.com/drive/v3`, and `apiRequest` cannot target another URL root, so a path such as `/upload/drive/v3/files` resolves to `https://www.googleapis.com/drive/v3/upload/drive/v3/files` and fails. Do not generate content upload code for this integration. Metadata-only creation through `POST /files` (for example, creating a folder) works. If an app must upload file content to Drive, ask the user to configure a REST API integration with base URL `https://www.googleapis.com/upload/drive/v3` using the same OAuth client, then call it through that integration.
|
|
386
|
+
|
|
412
387
|
### Folder MIME Type
|
|
413
388
|
|
|
414
389
|
Folders have a special MIME type:
|
|
@@ -481,4 +456,3 @@ try {
|
|
|
481
456
|
- [Google Drive API Documentation](https://developers.google.com/drive/api/reference/rest/v3)
|
|
482
457
|
- [Files Resource](https://developers.google.com/drive/api/reference/rest/v3/files)
|
|
483
458
|
- [Query String Syntax](https://developers.google.com/drive/api/guides/search-files)
|
|
484
|
-
- [Upload Files](https://developers.google.com/drive/api/guides/manage-uploads)
|
|
@@ -9,7 +9,10 @@ import type { z } from "zod";
|
|
|
9
9
|
|
|
10
10
|
import type { Plugin as GSheetsPlugin } from "@superblocksteam/types/dist/src/plugins/gsheets/v1/plugin_pb";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
RestApiValidationError,
|
|
14
|
+
restApiValidationErrorFromZodError,
|
|
15
|
+
} from "../../errors.js";
|
|
13
16
|
import { IntegrationError } from "../../runtime/errors.js";
|
|
14
17
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
15
18
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
@@ -113,12 +116,10 @@ export class GoogleSheetsClientImpl
|
|
|
113
116
|
const parseResult = schema.safeParse(result);
|
|
114
117
|
|
|
115
118
|
if (!parseResult.success) {
|
|
116
|
-
throw
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
data: result,
|
|
121
|
-
},
|
|
119
|
+
throw restApiValidationErrorFromZodError(
|
|
120
|
+
"Result validation failed",
|
|
121
|
+
parseResult.error,
|
|
122
|
+
result,
|
|
122
123
|
);
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -259,32 +259,6 @@ const result = await ctx.integrations.jira.apiRequest(
|
|
|
259
259
|
);
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
### Add Attachment
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// Note: Attachments require multipart/form-data
|
|
266
|
-
// The body should be the file content
|
|
267
|
-
const AttachmentResponseSchema = z.array(
|
|
268
|
-
z.object({
|
|
269
|
-
id: z.string(),
|
|
270
|
-
filename: z.string(),
|
|
271
|
-
size: z.number(),
|
|
272
|
-
}),
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
const result = await ctx.integrations.jira.apiRequest(
|
|
276
|
-
{
|
|
277
|
-
method: "POST",
|
|
278
|
-
path: `/rest/api/3/issue/${issueKey}/attachments`,
|
|
279
|
-
headers: {
|
|
280
|
-
"X-Atlassian-Token": "no-check",
|
|
281
|
-
},
|
|
282
|
-
body: fileContent, // Multipart form data
|
|
283
|
-
},
|
|
284
|
-
{ response: AttachmentResponseSchema },
|
|
285
|
-
);
|
|
286
|
-
```
|
|
287
|
-
|
|
288
262
|
## Trace Metadata
|
|
289
263
|
|
|
290
264
|
All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "jira",
|
|
3
3
|
"base": "README.md",
|
|
4
|
-
"overlays": [
|
|
4
|
+
"overlays": [
|
|
5
|
+
{
|
|
6
|
+
"file": "overlays/attachments.md",
|
|
7
|
+
"sdkVersionRange": ">=0.0.7"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"file": "overlays/attachments-unsupported.md",
|
|
11
|
+
"sdkVersionRange": "<0.0.7"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
5
14
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## Add Attachment
|
|
2
|
+
|
|
3
|
+
Attachments are multipart uploads, so pass `bodyType: "multipartForm"` and the `X-Atlassian-Token: no-check` header.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const AttachmentResponseSchema = z.array(
|
|
7
|
+
z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
filename: z.string(),
|
|
10
|
+
size: z.number(),
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const file = input.upload.files[0];
|
|
15
|
+
|
|
16
|
+
const result = await ctx.integrations.jira.apiRequest(
|
|
17
|
+
{
|
|
18
|
+
method: "POST",
|
|
19
|
+
path: `/rest/api/3/issue/${issueKey}/attachments`,
|
|
20
|
+
headers: { "X-Atlassian-Token": "no-check" },
|
|
21
|
+
bodyType: "multipartForm",
|
|
22
|
+
body: {
|
|
23
|
+
file: { value: await file.readContentsAsync("raw"), filename: file.name },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{ response: AttachmentResponseSchema },
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Do not set `Content-Type`. The worker generates the multipart boundary.
|
|
@@ -162,9 +162,7 @@ The `query()` method requires a Zod schema for runtime validation:
|
|
|
162
162
|
// WRONG - Missing schema parameter
|
|
163
163
|
const users = await ctx.integrations.db.query(
|
|
164
164
|
"SELECT * FROM users",
|
|
165
|
-
[
|
|
166
|
-
/* params */
|
|
167
|
-
], // This is wrong - params are 3rd argument
|
|
165
|
+
[/* params */], // This is wrong - params are 3rd argument
|
|
168
166
|
);
|
|
169
167
|
|
|
170
168
|
// CORRECT - Schema is the second parameter
|
|
@@ -9,7 +9,10 @@ import type { z } from "zod";
|
|
|
9
9
|
|
|
10
10
|
import type { Plugin as MongoDBPlugin } from "@superblocksteam/types/dist/src/plugins/mongodb/v1/plugin_pb";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
RestApiValidationError,
|
|
14
|
+
restApiValidationErrorFromZodError,
|
|
15
|
+
} from "../../errors.js";
|
|
13
16
|
import { IntegrationError } from "../../runtime/errors.js";
|
|
14
17
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
15
18
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
@@ -115,12 +118,10 @@ export class MongoDBClientImpl implements MongoDBClient, IntegrationClientImpl {
|
|
|
115
118
|
const parseResult = schema.safeParse(result);
|
|
116
119
|
|
|
117
120
|
if (!parseResult.success) {
|
|
118
|
-
throw
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
data: result,
|
|
123
|
-
},
|
|
121
|
+
throw restApiValidationErrorFromZodError(
|
|
122
|
+
"Result validation failed",
|
|
123
|
+
parseResult.error,
|
|
124
|
+
result,
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -292,9 +292,7 @@ The `query()` method requires a Zod schema for runtime validation. This is by de
|
|
|
292
292
|
// WRONG - Missing schema parameter
|
|
293
293
|
const users = await ctx.integrations.db.query(
|
|
294
294
|
"SELECT * FROM users",
|
|
295
|
-
[
|
|
296
|
-
/* params */
|
|
297
|
-
], // This is wrong - params are 3rd argument
|
|
295
|
+
[/* params */], // This is wrong - params are 3rd argument
|
|
298
296
|
);
|
|
299
297
|
|
|
300
298
|
// CORRECT - Schema is the second parameter
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
REDACTED_BINARY_RESPONSE_DATA,
|
|
6
|
+
RestApiValidationError,
|
|
7
|
+
} from "../../errors.js";
|
|
8
|
+
import type { IntegrationConfig } from "../types.js";
|
|
9
|
+
import { RestApiIntegrationPluginClientImpl } from "./client.js";
|
|
10
|
+
|
|
11
|
+
const TEST_CONFIG: IntegrationConfig = {
|
|
12
|
+
id: "restapi-test-id",
|
|
13
|
+
name: "Test REST API",
|
|
14
|
+
pluginId: "restapiintegration",
|
|
15
|
+
configuration: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const JsonResponseSchema = z.object({ ok: z.boolean() });
|
|
19
|
+
|
|
20
|
+
function createClient() {
|
|
21
|
+
const executeQuery = vi.fn().mockResolvedValue({ ok: true });
|
|
22
|
+
const client = new RestApiIntegrationPluginClientImpl(
|
|
23
|
+
TEST_CONFIG,
|
|
24
|
+
executeQuery,
|
|
25
|
+
);
|
|
26
|
+
return { client, executeQuery };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function callApiRequest(
|
|
30
|
+
client: RestApiIntegrationPluginClientImpl,
|
|
31
|
+
...args: unknown[]
|
|
32
|
+
): Promise<unknown> {
|
|
33
|
+
return Reflect.apply(client.apiRequest, client, args);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("RestApiIntegrationPluginClientImpl request body types", () => {
|
|
37
|
+
it("preserves omitted and explicit JSON body wire behavior", async () => {
|
|
38
|
+
for (const bodyType of [undefined, "json"] as const) {
|
|
39
|
+
const { client, executeQuery } = createClient();
|
|
40
|
+
await client.apiRequest(
|
|
41
|
+
{ method: "POST", path: "/payments", body: { amount: 42 }, bodyType },
|
|
42
|
+
{ response: JsonResponseSchema },
|
|
43
|
+
);
|
|
44
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
body: JSON.stringify({ amount: 42 }),
|
|
47
|
+
bodyType: "jsonBody",
|
|
48
|
+
}),
|
|
49
|
+
undefined,
|
|
50
|
+
undefined,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sends raw string bodies as rawBody", async () => {
|
|
56
|
+
const { client, executeQuery } = createClient();
|
|
57
|
+
await client.apiRequest(
|
|
58
|
+
{
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/xml",
|
|
61
|
+
body: "<note>hello</note>",
|
|
62
|
+
bodyType: "raw",
|
|
63
|
+
},
|
|
64
|
+
{ response: JsonResponseSchema },
|
|
65
|
+
);
|
|
66
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
body: "<note>hello</note>",
|
|
69
|
+
bodyType: "rawBody",
|
|
70
|
+
}),
|
|
71
|
+
undefined,
|
|
72
|
+
undefined,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("sends formUrlencoded bodies as rawBody with injected Content-Type", async () => {
|
|
77
|
+
const { client, executeQuery } = createClient();
|
|
78
|
+
await client.apiRequest(
|
|
79
|
+
{
|
|
80
|
+
method: "POST",
|
|
81
|
+
path: "/charges",
|
|
82
|
+
body: { amount: 2000, currency: "usd", "metadata[order_id]": "6735" },
|
|
83
|
+
bodyType: "formUrlencoded",
|
|
84
|
+
},
|
|
85
|
+
{ response: JsonResponseSchema },
|
|
86
|
+
);
|
|
87
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
body: "amount=2000¤cy=usd&metadata%5Border_id%5D=6735",
|
|
90
|
+
bodyType: "rawBody",
|
|
91
|
+
headers: [
|
|
92
|
+
{ key: "Content-Type", value: "application/x-www-form-urlencoded" },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
undefined,
|
|
96
|
+
undefined,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("appends the injected Content-Type after caller headers", async () => {
|
|
101
|
+
const { client, executeQuery } = createClient();
|
|
102
|
+
await client.apiRequest(
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/charges",
|
|
106
|
+
headers: { Authorization: "Bearer x" },
|
|
107
|
+
body: { amount: 2000 },
|
|
108
|
+
bodyType: "formUrlencoded",
|
|
109
|
+
},
|
|
110
|
+
{ response: JsonResponseSchema },
|
|
111
|
+
);
|
|
112
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
headers: [
|
|
115
|
+
{ key: "Authorization", value: "Bearer x" },
|
|
116
|
+
{ key: "Content-Type", value: "application/x-www-form-urlencoded" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
undefined,
|
|
120
|
+
undefined,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps a caller Content-Type of any case and injects nothing", async () => {
|
|
125
|
+
const { client, executeQuery } = createClient();
|
|
126
|
+
await client.apiRequest(
|
|
127
|
+
{
|
|
128
|
+
method: "POST",
|
|
129
|
+
path: "/charges",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
132
|
+
},
|
|
133
|
+
body: { amount: 2000 },
|
|
134
|
+
bodyType: "formUrlencoded",
|
|
135
|
+
},
|
|
136
|
+
{ response: JsonResponseSchema },
|
|
137
|
+
);
|
|
138
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
headers: [
|
|
141
|
+
{
|
|
142
|
+
key: "content-type",
|
|
143
|
+
value: "application/x-www-form-urlencoded; charset=utf-8",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
undefined,
|
|
148
|
+
undefined,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("sends binary bodies as base64 binaryBody with default Content-Type", async () => {
|
|
153
|
+
const bytes = Uint8Array.from([137, 80, 78, 71]);
|
|
154
|
+
const { client, executeQuery } = createClient();
|
|
155
|
+
await client.apiRequest(
|
|
156
|
+
{
|
|
157
|
+
method: "PUT",
|
|
158
|
+
path: "/files/report.png",
|
|
159
|
+
body: bytes,
|
|
160
|
+
bodyType: "binary",
|
|
161
|
+
},
|
|
162
|
+
{ response: JsonResponseSchema },
|
|
163
|
+
);
|
|
164
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
body: expect.any(String),
|
|
167
|
+
bodyType: "binaryBody",
|
|
168
|
+
headers: [{ key: "Content-Type", value: "application/octet-stream" }],
|
|
169
|
+
}),
|
|
170
|
+
undefined,
|
|
171
|
+
undefined,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("sends multipart bodies as formDataBase64 with base64 file parts and no top-level body", async () => {
|
|
176
|
+
const bytes = Uint8Array.from([137, 80, 78, 71]);
|
|
177
|
+
const { client, executeQuery } = createClient();
|
|
178
|
+
await client.apiRequest(
|
|
179
|
+
{
|
|
180
|
+
method: "POST",
|
|
181
|
+
path: "/upload",
|
|
182
|
+
body: {
|
|
183
|
+
note: "mixed",
|
|
184
|
+
file: { value: "contents", filename: "note.txt" },
|
|
185
|
+
image: { value: bytes, filename: "b.png" },
|
|
186
|
+
},
|
|
187
|
+
bodyType: "multipartForm",
|
|
188
|
+
},
|
|
189
|
+
{ response: JsonResponseSchema },
|
|
190
|
+
);
|
|
191
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
bodyType: "formDataBase64",
|
|
194
|
+
formData: [
|
|
195
|
+
{ key: "note", value: "mixed" },
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
key: "file",
|
|
198
|
+
file: { filename: "note.txt" },
|
|
199
|
+
}),
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
key: "image",
|
|
202
|
+
file: { filename: "b.png" },
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
undefined,
|
|
207
|
+
undefined,
|
|
208
|
+
);
|
|
209
|
+
expect(executeQuery.mock.calls[0][0]).not.toHaveProperty("body");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("rejects invalid bodyType and invalid bodies before executeQuery", async () => {
|
|
213
|
+
for (const bodyType of [
|
|
214
|
+
"form",
|
|
215
|
+
"jsonBody",
|
|
216
|
+
"rawBody",
|
|
217
|
+
"formData",
|
|
218
|
+
"fileForm",
|
|
219
|
+
"binaryBody",
|
|
220
|
+
"formDataBase64",
|
|
221
|
+
]) {
|
|
222
|
+
const { client, executeQuery } = createClient();
|
|
223
|
+
await expect(
|
|
224
|
+
callApiRequest(
|
|
225
|
+
client,
|
|
226
|
+
{ method: "POST", path: "/bad", body: {}, bodyType },
|
|
227
|
+
{ response: JsonResponseSchema },
|
|
228
|
+
),
|
|
229
|
+
).rejects.toThrow(RestApiValidationError);
|
|
230
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const invalidCases = [
|
|
234
|
+
{ method: "POST", path: "/bad", body: Uint8Array.from([1]) },
|
|
235
|
+
{ method: "POST", path: "/bad", bodyType: "raw" },
|
|
236
|
+
{ method: "POST", path: "/bad", bodyType: "formUrlencoded" },
|
|
237
|
+
{ method: "POST", path: "/bad", bodyType: "multipartForm" },
|
|
238
|
+
{ method: "POST", path: "/bad", bodyType: "binary" },
|
|
239
|
+
];
|
|
240
|
+
for (const options of invalidCases) {
|
|
241
|
+
const { client, executeQuery } = createClient();
|
|
242
|
+
await expect(
|
|
243
|
+
callApiRequest(client, options, { response: JsonResponseSchema }),
|
|
244
|
+
).rejects.toThrow(RestApiValidationError);
|
|
245
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("redacts request bodies that contain binary from bodySchema validation errors", async () => {
|
|
250
|
+
const bytes = Uint8Array.from([1, 2, 3, 4]);
|
|
251
|
+
const binaryBodies: {
|
|
252
|
+
body: unknown;
|
|
253
|
+
bodyType: string;
|
|
254
|
+
expectedData: unknown;
|
|
255
|
+
}[] = [
|
|
256
|
+
{
|
|
257
|
+
body: bytes,
|
|
258
|
+
bodyType: "binary",
|
|
259
|
+
expectedData: REDACTED_BINARY_RESPONSE_DATA,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
body: bytes.buffer,
|
|
263
|
+
bodyType: "binary",
|
|
264
|
+
expectedData: REDACTED_BINARY_RESPONSE_DATA,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
body: { file: { value: bytes, filename: "a.bin" }, note: "hello" },
|
|
268
|
+
bodyType: "multipartForm",
|
|
269
|
+
expectedData: {
|
|
270
|
+
file: { value: REDACTED_BINARY_RESPONSE_DATA, filename: "a.bin" },
|
|
271
|
+
note: "hello",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
body: { files: [{ value: bytes.buffer, filename: "a.bin" }] },
|
|
276
|
+
bodyType: "multipartForm",
|
|
277
|
+
expectedData: {
|
|
278
|
+
files: [{ value: REDACTED_BINARY_RESPONSE_DATA, filename: "a.bin" }],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
for (const { body, bodyType, expectedData } of binaryBodies) {
|
|
283
|
+
const { client, executeQuery } = createClient();
|
|
284
|
+
const error = await callApiRequest(
|
|
285
|
+
client,
|
|
286
|
+
{ method: "POST", path: "/upload", body, bodyType },
|
|
287
|
+
{ body: z.never(), response: JsonResponseSchema },
|
|
288
|
+
).catch((caught: unknown) => caught);
|
|
289
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
290
|
+
if (!(error instanceof RestApiValidationError)) throw error;
|
|
291
|
+
expect(error.details.data).toEqual(expectedData);
|
|
292
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const { client } = createClient();
|
|
296
|
+
const error = await callApiRequest(
|
|
297
|
+
client,
|
|
298
|
+
{ method: "POST", path: "/orders", body: { amount: "not-a-number" } },
|
|
299
|
+
{ body: z.object({ amount: z.number() }), response: JsonResponseSchema },
|
|
300
|
+
).catch((caught: unknown) => caught);
|
|
301
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
302
|
+
if (!(error instanceof RestApiValidationError)) throw error;
|
|
303
|
+
expect(error.details.data).toEqual({ amount: "not-a-number" });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("does not leak binary bytes into the thrown message or zodError for a literal-schema mismatch", async () => {
|
|
307
|
+
const bytes = Uint8Array.from([137, 80, 78, 71]);
|
|
308
|
+
const { client } = createClient();
|
|
309
|
+
const error = await callApiRequest(
|
|
310
|
+
client,
|
|
311
|
+
{ method: "PUT", path: "/upload", body: bytes, bodyType: "binary" },
|
|
312
|
+
{
|
|
313
|
+
body: z.literal("none"),
|
|
314
|
+
response: JsonResponseSchema,
|
|
315
|
+
},
|
|
316
|
+
).catch((caught: unknown) => caught);
|
|
317
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
318
|
+
if (!(error instanceof RestApiValidationError)) throw error;
|
|
319
|
+
expect(error.details.data).toBe(REDACTED_BINARY_RESPONSE_DATA);
|
|
320
|
+
expect(error.message).not.toContain("137");
|
|
321
|
+
expect(JSON.stringify(error.details.zodError.issues)).not.toContain("137");
|
|
322
|
+
});
|
|
323
|
+
});
|