@renderinc/sdk 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -2
- package/README.md +32 -22
- package/dist/experimental/experimental.d.ts +6 -2
- package/dist/experimental/experimental.d.ts.map +1 -1
- package/dist/experimental/experimental.js +9 -3
- package/dist/experimental/index.d.ts +2 -2
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +6 -5
- package/dist/experimental/object/api.d.ts +11 -0
- package/dist/experimental/object/api.d.ts.map +1 -0
- package/dist/experimental/object/api.js +44 -0
- package/dist/experimental/object/client.d.ts +21 -0
- package/dist/experimental/object/client.d.ts.map +1 -0
- package/dist/experimental/object/client.js +127 -0
- package/dist/experimental/object/index.d.ts +5 -0
- package/dist/experimental/object/index.d.ts.map +1 -0
- package/dist/experimental/object/index.js +8 -0
- package/dist/experimental/object/types.d.ts +49 -0
- package/dist/experimental/object/types.d.ts.map +1 -0
- package/dist/generated/schema.d.ts +131 -3
- package/dist/generated/schema.d.ts.map +1 -1
- package/dist/workflows/uds.d.ts.map +1 -1
- package/dist/workflows/uds.js +26 -51
- package/examples/client/main.ts +1 -1
- package/examples/client/package-lock.json +4 -4
- package/examples/client/package.json +1 -1
- package/examples/task/main.ts +1 -1
- package/examples/task/package-lock.json +5 -6
- package/examples/task/package.json +1 -1
- package/package.json +9 -8
- package/src/errors.test.ts +75 -0
- package/src/experimental/experimental.ts +30 -7
- package/src/experimental/index.ts +18 -18
- package/src/experimental/{blob → object}/api.ts +7 -7
- package/src/experimental/object/client.test.ts +138 -0
- package/src/experimental/{blob → object}/client.ts +57 -57
- package/src/experimental/object/index.ts +22 -0
- package/src/experimental/object/types.test.ts +87 -0
- package/src/experimental/{blob → object}/types.ts +30 -30
- package/src/generated/schema.ts +217 -9
- package/src/utils/get-base-url.test.ts +58 -0
- package/src/workflows/client/client.test.ts +68 -0
- package/src/workflows/types.test.ts +52 -0
- package/src/workflows/uds.ts +29 -69
- package/tsconfig.json +1 -1
- package/{vite.config.ts → vitest.config.ts} +1 -0
- package/dist/workflows/client/errors.d.ts +0 -25
- package/dist/workflows/client/errors.d.ts.map +0 -1
- package/dist/workflows/client/errors.js +0 -56
- package/dist/workflows/client/schema.d.ts +0 -9322
- package/dist/workflows/client/schema.d.ts.map +0 -1
- package/dist/workflows/client/workflows.d.ts +0 -15
- package/dist/workflows/client/workflows.d.ts.map +0 -1
- package/dist/workflows/client/workflows.js +0 -63
- package/src/experimental/blob/index.ts +0 -22
- /package/dist/{workflows/client/schema.js → experimental/object/types.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uds.d.ts","sourceRoot":"","sources":["../../src/workflows/uds.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"uds.d.ts","sourceRoot":"","sources":["../../src/workflows/uds.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,gBAAgB,EAEhB,wBAAwB,EAIxB,YAAY,EACb,MAAM,YAAY,CAAC;AAKpB,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAKzC,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAI3C,OAAO,CAAC,iBAAiB;IAyBnB,YAAY,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1D,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAa3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAUtE,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAa3C,OAAO;CA2CtB"}
|
package/dist/workflows/uds.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.UDSClient = void 0;
|
|
4
|
-
const
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
5
8
|
const version_js_1 = require("../version.js");
|
|
6
|
-
const CONTENT_LENGTH_REGEX = /Content-Length:\s*(\d+)/i;
|
|
7
9
|
class UDSClient {
|
|
8
10
|
constructor(socketPath) {
|
|
9
11
|
this.socketPath = socketPath;
|
|
@@ -59,66 +61,39 @@ class UDSClient {
|
|
|
59
61
|
}
|
|
60
62
|
async request(path, method, body) {
|
|
61
63
|
return new Promise((resolve, reject) => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const headers = data.substring(0, headerEndIndex);
|
|
80
|
-
const contentLengthMatch = headers.match(CONTENT_LENGTH_REGEX);
|
|
81
|
-
if (contentLengthMatch) {
|
|
82
|
-
contentLength = Number.parseInt(contentLengthMatch[1], 10);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
64
|
+
const req = node_http_1.default.request({
|
|
65
|
+
socketPath: this.socketPath,
|
|
66
|
+
path: path,
|
|
67
|
+
method: method,
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Length": body ? JSON.stringify(body).length : 0,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"User-Agent": (0, version_js_1.getUserAgent)(),
|
|
72
|
+
},
|
|
73
|
+
}, async (res) => {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for await (const chunk of res)
|
|
76
|
+
chunks.push(chunk);
|
|
77
|
+
const responseBody = Buffer.concat(chunks).toString();
|
|
78
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
79
|
+
reject(new Error(`HTTP ${res.statusCode}: ${responseBody}`));
|
|
80
|
+
return;
|
|
85
81
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
client.end();
|
|
90
|
-
}
|
|
82
|
+
if (responseBody.length === 0) {
|
|
83
|
+
resolve(undefined);
|
|
84
|
+
return;
|
|
91
85
|
}
|
|
92
|
-
});
|
|
93
|
-
client.on("end", () => {
|
|
94
86
|
try {
|
|
95
|
-
const lines = data.split("\r\n");
|
|
96
|
-
const statusLine = lines[0];
|
|
97
|
-
const statusCode = Number.parseInt(statusLine.split(" ")[1], 10);
|
|
98
|
-
if (statusCode >= 400) {
|
|
99
|
-
reject(new Error(`HTTP ${statusCode}: ${data}`));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const emptyLineIndex = lines.indexOf("");
|
|
103
|
-
if (emptyLineIndex === -1) {
|
|
104
|
-
resolve(undefined);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const bodyLines = lines.slice(emptyLineIndex + 1);
|
|
108
|
-
const responseBody = bodyLines.join("\r\n").trim();
|
|
109
|
-
if (!responseBody) {
|
|
110
|
-
resolve(undefined);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
87
|
resolve(JSON.parse(responseBody));
|
|
114
88
|
}
|
|
115
89
|
catch (error) {
|
|
116
90
|
reject(error);
|
|
117
91
|
}
|
|
118
92
|
});
|
|
119
|
-
|
|
93
|
+
req.on("error", (error) => {
|
|
120
94
|
reject(error);
|
|
121
95
|
});
|
|
96
|
+
req.end(body ? JSON.stringify(body) : "");
|
|
122
97
|
});
|
|
123
98
|
}
|
|
124
99
|
}
|
package/examples/client/main.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"name": "render-sdk-client-example",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@
|
|
11
|
+
"@renderinc/sdk": "file:../.."
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/node": "22.19.0",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"../..": {
|
|
19
|
-
"name": "@
|
|
19
|
+
"name": "@renderinc/sdk",
|
|
20
20
|
"version": "0.2.0",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/node": "^20.0.0",
|
|
29
29
|
"openapi-typescript": "^7.10.1",
|
|
30
30
|
"typescript": "^5.9.3",
|
|
31
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^4.0.18"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=18.0.0"
|
|
@@ -476,7 +476,7 @@
|
|
|
476
476
|
"node": ">=18"
|
|
477
477
|
}
|
|
478
478
|
},
|
|
479
|
-
"node_modules/@
|
|
479
|
+
"node_modules/@renderinc/sdk": {
|
|
480
480
|
"resolved": "../..",
|
|
481
481
|
"link": true
|
|
482
482
|
},
|
package/examples/task/main.ts
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@
|
|
12
|
+
"@renderinc/sdk": "file:../.."
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"tsx": "^4.0.0"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"../..": {
|
|
19
|
-
"name": "@
|
|
20
|
-
"version": "0.
|
|
19
|
+
"name": "@renderinc/sdk",
|
|
20
|
+
"version": "0.2.0",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"eventsource": "^4.0.0",
|
|
@@ -25,11 +25,10 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@biomejs/biome": "2.3.8",
|
|
28
|
-
"@types/eventsource": "^3.0.0",
|
|
29
28
|
"@types/node": "^20.0.0",
|
|
30
29
|
"openapi-typescript": "^7.10.1",
|
|
31
30
|
"typescript": "^5.9.3",
|
|
32
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^4.0.18"
|
|
33
32
|
},
|
|
34
33
|
"engines": {
|
|
35
34
|
"node": ">=18.0.0"
|
|
@@ -477,7 +476,7 @@
|
|
|
477
476
|
"node": ">=18"
|
|
478
477
|
}
|
|
479
478
|
},
|
|
480
|
-
"node_modules/@
|
|
479
|
+
"node_modules/@renderinc/sdk": {
|
|
481
480
|
"resolved": "../..",
|
|
482
481
|
"link": true
|
|
483
482
|
},
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renderinc/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Render SDK for TypeScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -p tsconfig.build.json",
|
|
9
9
|
"test": "vitest",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
10
11
|
"lint": "biome lint",
|
|
11
12
|
"lint:fix": "biome lint --write",
|
|
12
13
|
"format": "biome format --write",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"@types/node": "^20.0.0",
|
|
28
29
|
"openapi-typescript": "^7.10.1",
|
|
29
30
|
"typescript": "^5.9.3",
|
|
30
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^4.0.18"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"eventsource": "^4.0.0",
|
|
@@ -38,19 +39,19 @@
|
|
|
38
39
|
},
|
|
39
40
|
"exports": {
|
|
40
41
|
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
41
43
|
"import": "./dist/index.js",
|
|
42
|
-
"require": "./dist/index.js"
|
|
43
|
-
"types": "./dist/index.d.ts"
|
|
44
|
+
"require": "./dist/index.js"
|
|
44
45
|
},
|
|
45
46
|
"./workflows": {
|
|
47
|
+
"types": "./dist/workflows/index.d.ts",
|
|
46
48
|
"import": "./dist/workflows/index.js",
|
|
47
|
-
"require": "./dist/workflows/index.js"
|
|
48
|
-
"types": "./dist/workflows/index.d.ts"
|
|
49
|
+
"require": "./dist/workflows/index.js"
|
|
49
50
|
},
|
|
50
51
|
"./experimental": {
|
|
52
|
+
"types": "./dist/experimental/index.d.ts",
|
|
51
53
|
"import": "./dist/experimental/index.js",
|
|
52
|
-
"require": "./dist/experimental/index.js"
|
|
53
|
-
"types": "./dist/experimental/index.d.ts"
|
|
54
|
+
"require": "./dist/experimental/index.js"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbortError,
|
|
3
|
+
ClientError,
|
|
4
|
+
RenderError,
|
|
5
|
+
ServerError,
|
|
6
|
+
TaskRunError,
|
|
7
|
+
TimeoutError,
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
|
|
10
|
+
describe("errors", () => {
|
|
11
|
+
describe("RenderError", () => {
|
|
12
|
+
it("has correct name and message", () => {
|
|
13
|
+
const err = new RenderError("test message");
|
|
14
|
+
expect(err.name).toBe("RenderError");
|
|
15
|
+
expect(err.message).toBe("test message");
|
|
16
|
+
expect(err instanceof Error).toBe(true);
|
|
17
|
+
expect(err instanceof RenderError).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("TaskRunError", () => {
|
|
22
|
+
it("has correct name and properties", () => {
|
|
23
|
+
const err = new TaskRunError("task failed", "run-123", "internal error");
|
|
24
|
+
expect(err.name).toBe("TaskRunError");
|
|
25
|
+
expect(err.message).toBe("task failed");
|
|
26
|
+
expect(err.taskRunId).toBe("run-123");
|
|
27
|
+
expect(err.taskError).toBe("internal error");
|
|
28
|
+
expect(err instanceof RenderError).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("works with optional properties", () => {
|
|
32
|
+
const err = new TaskRunError("task failed");
|
|
33
|
+
expect(err.taskRunId).toBeUndefined();
|
|
34
|
+
expect(err.taskError).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("ClientError", () => {
|
|
39
|
+
it("has correct name and properties", () => {
|
|
40
|
+
const err = new ClientError("not found", 404, { detail: "missing" });
|
|
41
|
+
expect(err.name).toBe("ClientError");
|
|
42
|
+
expect(err.statusCode).toBe(404);
|
|
43
|
+
expect(err.response).toEqual({ detail: "missing" });
|
|
44
|
+
expect(err instanceof RenderError).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("ServerError", () => {
|
|
49
|
+
it("has correct name and properties", () => {
|
|
50
|
+
const err = new ServerError("server error", 500, { detail: "crash" });
|
|
51
|
+
expect(err.name).toBe("ServerError");
|
|
52
|
+
expect(err.statusCode).toBe(500);
|
|
53
|
+
expect(err.response).toEqual({ detail: "crash" });
|
|
54
|
+
expect(err instanceof RenderError).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("TimeoutError", () => {
|
|
59
|
+
it("has correct name and inherits from RenderError", () => {
|
|
60
|
+
const err = new TimeoutError("request timed out");
|
|
61
|
+
expect(err.name).toBe("TimeoutError");
|
|
62
|
+
expect(err.message).toBe("request timed out");
|
|
63
|
+
expect(err instanceof RenderError).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("AbortError", () => {
|
|
68
|
+
it("has correct name and fixed message", () => {
|
|
69
|
+
const err = new AbortError();
|
|
70
|
+
expect(err.name).toBe("AbortError");
|
|
71
|
+
expect(err.message).toBe("The operation was aborted.");
|
|
72
|
+
expect(err instanceof Error).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import type { Client } from "openapi-fetch";
|
|
2
2
|
import type { paths } from "../generated/schema.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ObjectClient } from "./object/client.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StorageClient provides access to experimental storage features
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Access object storage
|
|
11
|
+
* await render.experimental.storage.objects.put({
|
|
12
|
+
* ownerId: "tea-xxxxx",
|
|
13
|
+
* region: "oregon",
|
|
14
|
+
* key: "file.png",
|
|
15
|
+
* data: buffer
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class StorageClient {
|
|
20
|
+
/** Object storage client for managing binary objects */
|
|
21
|
+
public readonly objects: ObjectClient;
|
|
22
|
+
|
|
23
|
+
constructor(apiClient: Client<paths>) {
|
|
24
|
+
this.objects = new ObjectClient(apiClient);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
/**
|
|
6
29
|
* ExperimentalClient provides access to experimental Render SDK features
|
|
@@ -10,12 +33,12 @@ import { BlobClient } from "./blob/client.js";
|
|
|
10
33
|
*
|
|
11
34
|
* @example
|
|
12
35
|
* ```typescript
|
|
13
|
-
* import { Render } from '@
|
|
36
|
+
* import { Render } from '@renderinc/sdk';
|
|
14
37
|
*
|
|
15
38
|
* const render = new Render();
|
|
16
39
|
*
|
|
17
|
-
* // Access experimental
|
|
18
|
-
* await render.experimental.
|
|
40
|
+
* // Access experimental object storage
|
|
41
|
+
* await render.experimental.storage.objects.put({
|
|
19
42
|
* ownerId: "tea-xxxxx",
|
|
20
43
|
* region: "oregon",
|
|
21
44
|
* key: "file.png",
|
|
@@ -24,10 +47,10 @@ import { BlobClient } from "./blob/client.js";
|
|
|
24
47
|
* ```
|
|
25
48
|
*/
|
|
26
49
|
export class ExperimentalClient {
|
|
27
|
-
/**
|
|
28
|
-
public readonly
|
|
50
|
+
/** Storage client for managing storage features */
|
|
51
|
+
public readonly storage: StorageClient;
|
|
29
52
|
|
|
30
53
|
constructor(apiClient: Client<paths>) {
|
|
31
|
-
this.
|
|
54
|
+
this.storage = new StorageClient(apiClient);
|
|
32
55
|
}
|
|
33
56
|
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
// Experimental client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export { ExperimentalClient, StorageClient } from "./experimental.js";
|
|
4
|
+
// Object storage exports
|
|
4
5
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
6
|
+
type DeleteObjectInput,
|
|
7
|
+
type GetObjectInput,
|
|
8
|
+
ObjectApi,
|
|
9
|
+
ObjectClient,
|
|
10
|
+
type ObjectData,
|
|
11
|
+
type ObjectIdentifier,
|
|
12
|
+
type ObjectScope,
|
|
12
13
|
type PresignedDownloadUrl,
|
|
13
14
|
type PresignedUploadUrl,
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type
|
|
17
|
-
type
|
|
15
|
+
type PutObjectInput,
|
|
16
|
+
type PutObjectInputBuffer,
|
|
17
|
+
type PutObjectInputStream,
|
|
18
|
+
type PutObjectResult,
|
|
18
19
|
Region,
|
|
19
|
-
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
type
|
|
23
|
-
} from "./
|
|
24
|
-
export { ExperimentalClient } from "./experimental.js";
|
|
20
|
+
type ScopedDeleteObjectInput,
|
|
21
|
+
type ScopedGetObjectInput,
|
|
22
|
+
ScopedObjectClient,
|
|
23
|
+
type ScopedPutObjectInput,
|
|
24
|
+
} from "./object/index.js";
|
|
@@ -4,23 +4,23 @@ import type { paths } from "../../generated/schema.js";
|
|
|
4
4
|
import type { PresignedDownloadUrl, PresignedUploadUrl, Region } from "./types.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Layer 2: Typed
|
|
7
|
+
* Layer 2: Typed Object API Client
|
|
8
8
|
*
|
|
9
9
|
* Provides idiomatic TypeScript wrapper around the raw OpenAPI client.
|
|
10
10
|
* Handles presigned URL flow but still exposes the two-step nature
|
|
11
11
|
* (get URL, then upload/download). Useful for advanced use cases
|
|
12
12
|
* requiring fine-grained control.
|
|
13
13
|
*/
|
|
14
|
-
export class
|
|
14
|
+
export class ObjectApi {
|
|
15
15
|
constructor(private readonly apiClient: Client<paths>) {}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Get a presigned URL for uploading
|
|
18
|
+
* Get a presigned URL for uploading an object
|
|
19
19
|
*
|
|
20
20
|
* @param ownerId - Owner ID (workspace team ID)
|
|
21
21
|
* @param region - Storage region
|
|
22
22
|
* @param key - Object key (path)
|
|
23
|
-
* @param sizeBytes - Size of the
|
|
23
|
+
* @param sizeBytes - Size of the object in bytes
|
|
24
24
|
* @returns Presigned upload URL with expiration and size limit
|
|
25
25
|
*/
|
|
26
26
|
async getUploadUrl(
|
|
@@ -46,7 +46,7 @@ export class BlobApi {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Get a presigned URL for downloading
|
|
49
|
+
* Get a presigned URL for downloading an object
|
|
50
50
|
*
|
|
51
51
|
* @param ownerId - Owner ID (workspace team ID)
|
|
52
52
|
* @param region - Storage region
|
|
@@ -73,7 +73,7 @@ export class BlobApi {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Delete
|
|
76
|
+
* Delete an object
|
|
77
77
|
*
|
|
78
78
|
* @param ownerId - Owner ID (workspace team ID)
|
|
79
79
|
* @param region - Storage region
|
|
@@ -85,7 +85,7 @@ export class BlobApi {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
if (error) {
|
|
88
|
-
throw new RenderError(`Failed to delete
|
|
88
|
+
throw new RenderError(`Failed to delete object: ${error.message || "Unknown error"}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import type { Client } from "openapi-fetch";
|
|
3
|
+
import { RenderError } from "../../errors.js";
|
|
4
|
+
import type { paths } from "../../generated/schema.js";
|
|
5
|
+
import { ObjectClient } from "./client.js";
|
|
6
|
+
import type { PutObjectInput } from "./types.js";
|
|
7
|
+
|
|
8
|
+
describe("ObjectClient", () => {
|
|
9
|
+
describe("resolveSize (via put validation)", () => {
|
|
10
|
+
// Test resolveSize indirectly by calling put() which will fail
|
|
11
|
+
// at the API call stage, but size validation happens first.
|
|
12
|
+
const putMock = vi.fn().mockRejectedValue(new Error("should not reach API"));
|
|
13
|
+
const mockApiClient = { PUT: putMock } as unknown as Client<paths>;
|
|
14
|
+
|
|
15
|
+
const client = new ObjectClient(mockApiClient);
|
|
16
|
+
|
|
17
|
+
it("auto-calculates Buffer size", async () => {
|
|
18
|
+
const buffer = Buffer.from("hello");
|
|
19
|
+
putMock.mockResolvedValueOnce({
|
|
20
|
+
data: { url: "http://test" },
|
|
21
|
+
error: null,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Will fail at fetch, but that's after size validation
|
|
25
|
+
await expect(
|
|
26
|
+
client.put({
|
|
27
|
+
ownerId: "tea-test",
|
|
28
|
+
region: "oregon",
|
|
29
|
+
key: "test.txt",
|
|
30
|
+
data: buffer,
|
|
31
|
+
}),
|
|
32
|
+
).rejects.toThrow(); // fetch not available in test
|
|
33
|
+
|
|
34
|
+
expect(putMock).toHaveBeenCalledWith(
|
|
35
|
+
"/blobs/{ownerId}/{region}/{key}",
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
body: { sizeBytes: 5 },
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("auto-calculates Uint8Array size", async () => {
|
|
43
|
+
const arr = new Uint8Array([1, 2, 3, 4]);
|
|
44
|
+
putMock.mockResolvedValueOnce({
|
|
45
|
+
data: { url: "http://test" },
|
|
46
|
+
error: null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await expect(
|
|
50
|
+
client.put({
|
|
51
|
+
ownerId: "tea-test",
|
|
52
|
+
region: "oregon",
|
|
53
|
+
key: "test.bin",
|
|
54
|
+
data: arr,
|
|
55
|
+
}),
|
|
56
|
+
).rejects.toThrow();
|
|
57
|
+
|
|
58
|
+
expect(putMock).toHaveBeenCalledWith(
|
|
59
|
+
"/blobs/{ownerId}/{region}/{key}",
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
body: { sizeBytes: 4 },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("throws on size mismatch for Buffer", async () => {
|
|
67
|
+
const buffer = Buffer.from("hello");
|
|
68
|
+
await expect(
|
|
69
|
+
client.put({
|
|
70
|
+
ownerId: "tea-test",
|
|
71
|
+
region: "oregon",
|
|
72
|
+
key: "test.txt",
|
|
73
|
+
data: buffer,
|
|
74
|
+
size: 10,
|
|
75
|
+
}),
|
|
76
|
+
).rejects.toThrow(RenderError);
|
|
77
|
+
await expect(
|
|
78
|
+
client.put({
|
|
79
|
+
ownerId: "tea-test",
|
|
80
|
+
region: "oregon",
|
|
81
|
+
key: "test.txt",
|
|
82
|
+
data: buffer,
|
|
83
|
+
size: 10,
|
|
84
|
+
}),
|
|
85
|
+
).rejects.toThrow("Size mismatch");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("requires size for stream input", async () => {
|
|
89
|
+
const stream = Readable.from(["hello"]);
|
|
90
|
+
const invalidInput = {
|
|
91
|
+
ownerId: "tea-test",
|
|
92
|
+
region: "oregon",
|
|
93
|
+
key: "test.txt",
|
|
94
|
+
data: stream,
|
|
95
|
+
// Intentionally omit size to test validation (invalid at runtime)
|
|
96
|
+
} as unknown as PutObjectInput;
|
|
97
|
+
await expect(client.put(invalidInput)).rejects.toThrow(RenderError);
|
|
98
|
+
await expect(client.put(invalidInput)).rejects.toThrow("Size is required");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("requires size for string input", async () => {
|
|
102
|
+
await expect(
|
|
103
|
+
client.put({
|
|
104
|
+
ownerId: "tea-test",
|
|
105
|
+
region: "oregon",
|
|
106
|
+
key: "test.txt",
|
|
107
|
+
data: "hello",
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow("Size is required");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("throws on zero size", async () => {
|
|
113
|
+
const stream = Readable.from(["hello"]);
|
|
114
|
+
await expect(
|
|
115
|
+
client.put({
|
|
116
|
+
ownerId: "tea-test",
|
|
117
|
+
region: "oregon",
|
|
118
|
+
key: "test.txt",
|
|
119
|
+
data: stream,
|
|
120
|
+
size: 0,
|
|
121
|
+
}),
|
|
122
|
+
).rejects.toThrow("Size must be a positive integer");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("throws on negative size", async () => {
|
|
126
|
+
const stream = Readable.from(["hello"]);
|
|
127
|
+
await expect(
|
|
128
|
+
client.put({
|
|
129
|
+
ownerId: "tea-test",
|
|
130
|
+
region: "oregon",
|
|
131
|
+
key: "test.txt",
|
|
132
|
+
data: stream,
|
|
133
|
+
size: -1,
|
|
134
|
+
}),
|
|
135
|
+
).rejects.toThrow("Size must be a positive integer");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|