@superblocksteam/sdk-api 0.0.12-body-types → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +23 -0
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +1 -44
  4. package/dist/errors.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/base/index.d.ts +1 -2
  9. package/dist/integrations/base/index.d.ts.map +1 -1
  10. package/dist/integrations/base/index.js +0 -1
  11. package/dist/integrations/base/index.js.map +1 -1
  12. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  13. package/dist/integrations/base/rest-api-client-base.js +4 -18
  14. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  15. package/dist/integrations/base/types.d.ts +5 -40
  16. package/dist/integrations/base/types.d.ts.map +1 -1
  17. package/dist/integrations/base/types.js +0 -7
  18. package/dist/integrations/base/types.js.map +1 -1
  19. package/dist/integrations/documentation-resolver.test.js +1 -20
  20. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  21. package/dist/integrations/dynamodb/client.d.ts +6 -2
  22. package/dist/integrations/dynamodb/client.d.ts.map +1 -1
  23. package/dist/integrations/dynamodb/client.js +83 -10
  24. package/dist/integrations/dynamodb/client.js.map +1 -1
  25. package/dist/integrations/dynamodb/client.test.d.ts +8 -0
  26. package/dist/integrations/dynamodb/client.test.d.ts.map +1 -0
  27. package/dist/integrations/dynamodb/client.test.js +198 -0
  28. package/dist/integrations/dynamodb/client.test.js.map +1 -0
  29. package/dist/integrations/dynamodb/index.d.ts +1 -1
  30. package/dist/integrations/dynamodb/index.d.ts.map +1 -1
  31. package/dist/integrations/dynamodb/index.js.map +1 -1
  32. package/dist/integrations/dynamodb/types.d.ts +27 -1
  33. package/dist/integrations/dynamodb/types.d.ts.map +1 -1
  34. package/dist/integrations/index.d.ts +1 -1
  35. package/dist/integrations/index.d.ts.map +1 -1
  36. package/dist/integrations/index.js.map +1 -1
  37. package/dist/integrations/slack/client.test.js +0 -23
  38. package/dist/integrations/slack/client.test.js.map +1 -1
  39. package/dist/runtime/context.d.ts +6 -0
  40. package/dist/runtime/context.d.ts.map +1 -1
  41. package/dist/runtime/context.js +2 -1
  42. package/dist/runtime/context.js.map +1 -1
  43. package/dist/runtime/executor.d.ts +6 -0
  44. package/dist/runtime/executor.d.ts.map +1 -1
  45. package/dist/runtime/executor.js +1 -0
  46. package/dist/runtime/executor.js.map +1 -1
  47. package/dist/types.d.ts +10 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/errors.ts +1 -52
  51. package/src/index.ts +1 -0
  52. package/src/integrations/base/index.ts +0 -7
  53. package/src/integrations/base/rest-api-client-base.ts +4 -18
  54. package/src/integrations/base/types.ts +5 -59
  55. package/src/integrations/documentation-resolver.test.ts +1 -24
  56. package/src/integrations/dynamodb/README.md +33 -12
  57. package/src/integrations/dynamodb/client.test.ts +254 -0
  58. package/src/integrations/dynamodb/client.ts +121 -16
  59. package/src/integrations/dynamodb/index.ts +5 -1
  60. package/src/integrations/dynamodb/types.ts +33 -1
  61. package/src/integrations/index.ts +1 -0
  62. package/src/integrations/restapiintegration/docs.manifest.json +0 -8
  63. package/src/integrations/slack/client.test.ts +0 -33
  64. package/src/runtime/context.ts +9 -0
  65. package/src/runtime/executor.ts +8 -0
  66. package/src/types.ts +11 -0
  67. package/dist/integrations/base/request-body.d.ts +0 -21
  68. package/dist/integrations/base/request-body.d.ts.map +0 -1
  69. package/dist/integrations/base/request-body.js +0 -322
  70. package/dist/integrations/base/request-body.js.map +0 -1
  71. package/dist/integrations/base/request-body.test.d.ts +0 -2
  72. package/dist/integrations/base/request-body.test.d.ts.map +0 -1
  73. package/dist/integrations/base/request-body.test.js +0 -324
  74. package/dist/integrations/base/request-body.test.js.map +0 -1
  75. package/dist/integrations/restapiintegration/client.body-types.test.d.ts +0 -2
  76. package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +0 -1
  77. package/dist/integrations/restapiintegration/client.body-types.test.js +0 -145
  78. package/dist/integrations/restapiintegration/client.body-types.test.js.map +0 -1
  79. package/src/integrations/base/request-body.test.ts +0 -402
  80. package/src/integrations/base/request-body.ts +0 -548
  81. package/src/integrations/restapiintegration/client.body-types.test.ts +0 -214
  82. package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +0 -7
  83. package/src/integrations/restapiintegration/overlays/request-body-types.md +0 -106
@@ -1,402 +0,0 @@
1
- import { describe, expect, expectTypeOf, it } from "vitest";
2
-
3
- import { RestApiBodyDataType } from "@superblocksteam/shared";
4
-
5
- import {
6
- REDACTED_BINARY_RESPONSE_DATA,
7
- RestApiValidationError,
8
- } from "../../errors.js";
9
- import { encodeRequestBody, MAX_BINARY_BODY_BYTES } from "./request-body.js";
10
- import { REST_API_BODY_TYPES, type RestApiBodyType } from "./types.js";
11
-
12
- const PNG_BYTES = [137, 80, 78, 71];
13
- const PNG_BASE64 = Buffer.from(PNG_BYTES).toString("base64");
14
- const AGENT_REQUEST_CAP_BYTES = 28 * 1024 * 1024;
15
-
16
- function arrayBufferFrom(bytes: readonly number[]): ArrayBuffer {
17
- const buffer = new ArrayBuffer(bytes.length);
18
- new Uint8Array(buffer).set(bytes);
19
- return buffer;
20
- }
21
-
22
- function getRestApiValidationError(fn: () => unknown): RestApiValidationError {
23
- try {
24
- fn();
25
- } catch (error) {
26
- if (error instanceof RestApiValidationError) return error;
27
- throw error;
28
- }
29
- throw new Error("Expected RestApiValidationError");
30
- }
31
-
32
- function expectBinaryDataRedacted(data: unknown): void {
33
- if (data !== REDACTED_BINARY_RESPONSE_DATA) {
34
- throw new Error("Expected binary validation data to be redacted");
35
- }
36
- }
37
-
38
- describe("encodeRequestBody", () => {
39
- it("keeps the public body type contract SDK-owned", () => {
40
- expect(REST_API_BODY_TYPES).toEqual([
41
- "json",
42
- "raw",
43
- "formUrlencoded",
44
- "multipartForm",
45
- "binary",
46
- ]);
47
- expectTypeOf<RestApiBodyType>().toEqualTypeOf<
48
- "json" | "raw" | "formUrlencoded" | "multipartForm" | "binary"
49
- >();
50
- expect(Object.values(RestApiBodyDataType).sort()).toEqual([
51
- "fileForm",
52
- "formData",
53
- "jsonBody",
54
- "rawBody",
55
- ]);
56
- expect(Object.values(RestApiBodyDataType)).not.toContain("binaryBody");
57
- expect(Object.values(RestApiBodyDataType)).not.toContain("formDataBase64");
58
- });
59
-
60
- it("preserves default and explicit JSON wire behavior", () => {
61
- expect(encodeRequestBody({ body: undefined })).toEqual({
62
- injectedHeaders: [],
63
- });
64
- expect(encodeRequestBody({ body: undefined, bodyType: "json" })).toEqual({
65
- injectedHeaders: [],
66
- });
67
- expect(encodeRequestBody({ body: { amount: 42 } })).toEqual({
68
- body: JSON.stringify({ amount: 42 }),
69
- bodyType: "jsonBody",
70
- injectedHeaders: [],
71
- });
72
- expect(
73
- encodeRequestBody({ body: { amount: 42 }, bodyType: "json" }),
74
- ).toEqual({
75
- body: JSON.stringify({ amount: 42 }),
76
- bodyType: "jsonBody",
77
- injectedHeaders: [],
78
- });
79
- });
80
-
81
- it("rejects invalid JSON and bodyType inputs", () => {
82
- expect(() =>
83
- encodeRequestBody({ body: Uint8Array.from(PNG_BYTES) }),
84
- ).toThrow(/bodyType: "binary"/);
85
- const circular: Record<string, unknown> = {};
86
- circular.self = circular;
87
- for (const body of [circular, 1n, () => undefined]) {
88
- expect(() => encodeRequestBody({ body })).toThrow(RestApiValidationError);
89
- }
90
- for (const bodyType of [
91
- "jsonBody",
92
- "rawBody",
93
- "formData",
94
- "fileForm",
95
- "binaryBody",
96
- "formDataBase64",
97
- ]) {
98
- expect(() => encodeRequestBody({ body: {}, bodyType })).toThrow(
99
- new RegExp(`Unsupported bodyType ${JSON.stringify(bodyType)}`),
100
- );
101
- }
102
- });
103
-
104
- it("passes raw non-empty string bodies through unchanged", () => {
105
- expect(
106
- encodeRequestBody({ body: "<note>hello</note>", bodyType: "raw" }),
107
- ).toEqual({
108
- body: "<note>hello</note>",
109
- bodyType: "rawBody",
110
- injectedHeaders: [],
111
- });
112
- expect(() => encodeRequestBody({ body: "", bodyType: "raw" })).toThrow(
113
- RestApiValidationError,
114
- );
115
- expect(() =>
116
- encodeRequestBody({ body: { value: "hello" }, bodyType: "raw" }),
117
- ).toThrow(RestApiValidationError);
118
- });
119
-
120
- it("encodes form-urlencoded bodies and injects Content-Type only when absent", () => {
121
- expect(
122
- encodeRequestBody({
123
- body: {
124
- amount: 2000,
125
- currency: "usd",
126
- empty: [],
127
- enabled: true,
128
- optional: undefined,
129
- "metadata[order_id]": "6735",
130
- tag: ["vip", "repeat"],
131
- },
132
- bodyType: "formUrlencoded",
133
- }),
134
- ).toEqual({
135
- body: "amount=2000&currency=usd&enabled=true&metadata%5Border_id%5D=6735&tag=vip&tag=repeat",
136
- bodyType: "rawBody",
137
- injectedHeaders: [
138
- { key: "Content-Type", value: "application/x-www-form-urlencoded" },
139
- ],
140
- });
141
- expect(
142
- encodeRequestBody({
143
- body: { amount: 2000 },
144
- bodyType: "formUrlencoded",
145
- headers: { "content-type": "application/custom-form" },
146
- }),
147
- ).toEqual({
148
- body: "amount=2000",
149
- bodyType: "rawBody",
150
- injectedHeaders: [],
151
- });
152
- });
153
-
154
- it("rejects invalid form-urlencoded bodies", () => {
155
- const invalidBodies = [
156
- undefined,
157
- [],
158
- { "": "missing" },
159
- { amount: Number.NaN },
160
- { metadata: { orderId: "6735" } },
161
- { empty: [] },
162
- ];
163
- for (const body of invalidBodies) {
164
- expect(() =>
165
- encodeRequestBody({ body, bodyType: "formUrlencoded" }),
166
- ).toThrow(RestApiValidationError);
167
- }
168
- expect(() =>
169
- encodeRequestBody({
170
- body: { metadata: { orderId: "6735" } },
171
- bodyType: "formUrlencoded",
172
- }),
173
- ).toThrow(/metadata\[order_id\]/);
174
- });
175
-
176
- it.each([
177
- ["Uint8Array", Uint8Array.from(PNG_BYTES)],
178
- ["ArrayBuffer", arrayBufferFrom(PNG_BYTES)],
179
- ["DataView", new DataView(arrayBufferFrom([0, ...PNG_BYTES, 1]), 1, 4)],
180
- ["subarray view", Uint8Array.from([0, ...PNG_BYTES, 1]).subarray(1, 5)],
181
- ["Buffer", Buffer.from(PNG_BYTES)],
182
- ])("base64-encodes exact bytes from %s", (_name, body) => {
183
- expect(encodeRequestBody({ body, bodyType: "binary" })).toEqual({
184
- body: PNG_BASE64,
185
- bodyType: "binaryBody",
186
- injectedHeaders: [
187
- { key: "Content-Type", value: "application/octet-stream" },
188
- ],
189
- });
190
- });
191
-
192
- it("rejects invalid binary bodies and preserves caller Content-Type", () => {
193
- expect(
194
- encodeRequestBody({
195
- body: Uint8Array.from(PNG_BYTES),
196
- bodyType: "binary",
197
- headers: { "CONTENT-TYPE": "image/png" },
198
- }),
199
- ).toEqual({
200
- body: PNG_BASE64,
201
- bodyType: "binaryBody",
202
- injectedHeaders: [],
203
- });
204
- for (const body of [
205
- new ArrayBuffer(0),
206
- "not bytes",
207
- new Uint8Array(MAX_BINARY_BODY_BYTES + 1),
208
- ]) {
209
- expect(() => encodeRequestBody({ body, bodyType: "binary" })).toThrow(
210
- RestApiValidationError,
211
- );
212
- }
213
- });
214
-
215
- it("keeps the binary request cap below the serialized agent request cap", () => {
216
- expect(Math.ceil(MAX_BINARY_BODY_BYTES / 3) * 4).toBeLessThan(
217
- AGENT_REQUEST_CAP_BYTES,
218
- );
219
- });
220
-
221
- it("redacts binary validation data for invalid request bodies", () => {
222
- const binaryError = getRestApiValidationError(() =>
223
- encodeRequestBody({
224
- body: new ArrayBuffer(MAX_BINARY_BODY_BYTES + 1),
225
- bodyType: "binary",
226
- }),
227
- );
228
- expectBinaryDataRedacted(binaryError.details.data);
229
-
230
- const multipartError = getRestApiValidationError(() =>
231
- encodeRequestBody({
232
- body: {
233
- file: {
234
- value: new Uint8Array(MAX_BINARY_BODY_BYTES + 1),
235
- filename: "too-large.bin",
236
- },
237
- },
238
- bodyType: "multipartForm",
239
- }),
240
- );
241
- expectBinaryDataRedacted(multipartError.details.data);
242
-
243
- const invalidPartError = getRestApiValidationError(() =>
244
- encodeRequestBody({
245
- body: {
246
- file: {
247
- contentType: "application/octet-stream",
248
- filename: "too-large.bin",
249
- value: Uint8Array.from(PNG_BYTES),
250
- },
251
- },
252
- bodyType: "multipartForm",
253
- }),
254
- );
255
- expect(invalidPartError.details.data).toEqual({
256
- contentType: "application/octet-stream",
257
- filename: "too-large.bin",
258
- value: REDACTED_BINARY_RESPONSE_DATA,
259
- });
260
- });
261
-
262
- it("encodes multipart primitive, string-file, and mixed binary file parts", () => {
263
- expect(
264
- encodeRequestBody({
265
- body: { active: true, amount: 42, note: "hello" },
266
- bodyType: "multipartForm",
267
- }),
268
- ).toEqual({
269
- bodyType: "formData",
270
- formData: [
271
- { key: "active", value: "true" },
272
- { key: "amount", value: "42" },
273
- { key: "note", value: "hello" },
274
- ],
275
- injectedHeaders: [],
276
- });
277
- expect(
278
- encodeRequestBody({
279
- body: {
280
- empty: { value: "", filename: "empty.txt" },
281
- file: {
282
- value: "hello",
283
- filename: "hello.txt",
284
- contentType: undefined,
285
- },
286
- },
287
- bodyType: "multipartForm",
288
- }),
289
- ).toEqual({
290
- bodyType: "formData",
291
- formData: [
292
- { key: "empty", value: "", file: { filename: "empty.txt" } },
293
- { key: "file", value: "hello", file: { filename: "hello.txt" } },
294
- ],
295
- injectedHeaders: [],
296
- });
297
- expect(
298
- encodeRequestBody({
299
- body: {
300
- image: { value: Uint8Array.from(PNG_BYTES), filename: "b.png" },
301
- note: "mixed",
302
- textFile: { value: "cafe \u00e9", filename: "note.txt" },
303
- },
304
- bodyType: "multipartForm",
305
- }),
306
- ).toEqual({
307
- bodyType: "formDataBase64",
308
- formData: [
309
- { key: "image", value: PNG_BASE64, file: { filename: "b.png" } },
310
- { key: "note", value: "mixed" },
311
- {
312
- key: "textFile",
313
- value: Buffer.from("cafe \u00e9", "utf8").toString("base64"),
314
- file: { filename: "note.txt" },
315
- },
316
- ],
317
- injectedHeaders: [],
318
- });
319
- });
320
-
321
- it("rejects malformed multipart bodies", () => {
322
- expect(() =>
323
- encodeRequestBody({
324
- body: { file: "hello" },
325
- bodyType: "multipartForm",
326
- headers: { "Content-Type": "multipart/form-data" },
327
- }),
328
- ).toThrow(/boundary/);
329
- const invalidBodies = [
330
- {},
331
- { "": "missing" },
332
- { file: Uint8Array.from(PNG_BYTES) },
333
- {
334
- file: {
335
- contentType: "image/png",
336
- filename: "b.png",
337
- value: Uint8Array.from(PNG_BYTES),
338
- },
339
- },
340
- { file: { value: "hello", filename: "" } },
341
- {
342
- file: {
343
- value: new Uint8Array(MAX_BINARY_BODY_BYTES + 1),
344
- filename: "too-large.bin",
345
- },
346
- },
347
- ];
348
- for (const body of invalidBodies) {
349
- expect(() =>
350
- encodeRequestBody({ body, bodyType: "multipartForm" }),
351
- ).toThrow(RestApiValidationError);
352
- }
353
- expect(() =>
354
- encodeRequestBody({
355
- body: { file: { value: "contents" } },
356
- bodyType: "multipartForm",
357
- }),
358
- ).toThrow(/missing required key: filename/);
359
- expect(() =>
360
- encodeRequestBody({
361
- body: {
362
- file: {
363
- value: "contents",
364
- filename: "contents.txt",
365
- contentType: "text/plain",
366
- },
367
- },
368
- bodyType: "multipartForm",
369
- }),
370
- ).toThrow(/unexpected key: contentType/);
371
- });
372
-
373
- it("rejects multipart bodies whose total file-part bytes exceed the request budget", () => {
374
- const partBytes = Math.floor(MAX_BINARY_BODY_BYTES / 2) + 1;
375
- const first = "a".repeat(partBytes);
376
- const second = "b".repeat(partBytes);
377
-
378
- expect(() =>
379
- encodeRequestBody({
380
- body: {
381
- first: { value: first, filename: "first.txt" },
382
- second: { value: second, filename: "second.txt" },
383
- },
384
- bodyType: "multipartForm",
385
- }),
386
- ).toThrow(/20 MiB/);
387
- });
388
-
389
- it("rejects raw and multipart field bodies that exceed the request budget", () => {
390
- const tooLarge = "x".repeat(MAX_BINARY_BODY_BYTES + 1);
391
-
392
- expect(() =>
393
- encodeRequestBody({ body: tooLarge, bodyType: "raw" }),
394
- ).toThrow(/request bodies must be 20 MiB or smaller/);
395
- expect(() =>
396
- encodeRequestBody({
397
- body: { csv: tooLarge },
398
- bodyType: "multipartForm",
399
- }),
400
- ).toThrow(/request bodies must be 20 MiB or smaller/);
401
- });
402
- });