@renderinc/sdk 0.1.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 +33 -0
- package/README.md +17 -7
- package/biome.json +84 -0
- package/dist/experimental/blob/api.d.ts +11 -0
- package/dist/experimental/blob/api.d.ts.map +1 -0
- package/dist/experimental/blob/api.js +44 -0
- package/dist/experimental/blob/client.d.ts +21 -0
- package/dist/experimental/blob/client.d.ts.map +1 -0
- package/dist/experimental/blob/client.js +127 -0
- package/dist/experimental/blob/index.d.ts +5 -0
- package/dist/experimental/blob/index.d.ts.map +1 -0
- package/dist/experimental/blob/index.js +8 -0
- package/dist/experimental/blob/types.d.ts +49 -0
- package/dist/experimental/blob/types.d.ts.map +1 -0
- package/dist/experimental/experimental.d.ts +12 -0
- package/dist/experimental/experimental.d.ts.map +1 -0
- package/dist/experimental/experimental.js +16 -0
- package/dist/experimental/index.d.ts +3 -0
- package/dist/experimental/index.d.ts.map +1 -0
- package/dist/experimental/index.js +10 -0
- 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/experimental/object/types.js +2 -0
- package/dist/generated/schema.d.ts +9910 -0
- package/dist/generated/schema.d.ts.map +1 -0
- package/dist/generated/schema.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/render.d.ts +2 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +4 -2
- package/dist/utils/create-api-client.d.ts +1 -1
- package/dist/utils/create-api-client.d.ts.map +1 -1
- package/dist/utils/create-api-client.js +2 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/workflows/client/client.d.ts +1 -1
- package/dist/workflows/client/client.d.ts.map +1 -1
- package/dist/workflows/client/sse.d.ts +2 -2
- package/dist/workflows/client/sse.d.ts.map +1 -1
- package/dist/workflows/client/sse.js +2 -0
- package/dist/workflows/client/types.d.ts +1 -1
- package/dist/workflows/client/types.d.ts.map +1 -1
- package/dist/workflows/executor.d.ts +2 -2
- package/dist/workflows/executor.d.ts.map +1 -1
- package/dist/workflows/registry.d.ts +1 -1
- package/dist/workflows/registry.d.ts.map +1 -1
- package/dist/workflows/registry.js +13 -6
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +2 -0
- package/dist/workflows/schema.d.ts +9 -0
- package/dist/workflows/schema.d.ts.map +1 -1
- package/dist/workflows/task.d.ts +1 -0
- package/dist/workflows/task.d.ts.map +1 -1
- package/dist/workflows/task.js +34 -0
- package/dist/workflows/types.d.ts +3 -1
- package/dist/workflows/types.d.ts.map +1 -1
- package/dist/workflows/uds.d.ts +1 -1
- package/dist/workflows/uds.d.ts.map +1 -1
- package/dist/workflows/uds.js +27 -82
- package/examples/client/main.ts +42 -0
- package/examples/client/package-lock.json +601 -0
- package/examples/client/package.json +16 -0
- package/examples/client/tsconfig.json +17 -0
- package/examples/task/main.ts +90 -0
- package/examples/task/package-lock.json +584 -0
- package/examples/task/package.json +16 -0
- package/examples/task/tsconfig.json +17 -0
- package/package.json +19 -27
- package/src/errors.test.ts +75 -0
- package/src/errors.ts +73 -0
- package/src/experimental/experimental.ts +56 -0
- package/src/experimental/index.ts +24 -0
- package/src/experimental/object/api.ts +91 -0
- package/src/experimental/object/client.test.ts +138 -0
- package/src/experimental/object/client.ts +317 -0
- package/src/experimental/object/index.ts +22 -0
- package/src/experimental/object/types.test.ts +87 -0
- package/src/experimental/object/types.ts +131 -0
- package/src/generated/schema.ts +12937 -0
- package/src/index.ts +7 -0
- package/src/render.ts +35 -0
- package/src/utils/create-api-client.ts +13 -0
- package/src/utils/get-base-url.test.ts +58 -0
- package/src/utils/get-base-url.ts +16 -0
- package/src/version.ts +37 -0
- package/src/workflows/client/client.test.ts +68 -0
- package/src/workflows/client/client.ts +142 -0
- package/src/workflows/client/create-client.ts +17 -0
- package/src/workflows/client/index.ts +3 -0
- package/src/workflows/client/sse.ts +95 -0
- package/src/workflows/client/types.ts +56 -0
- package/src/workflows/executor.ts +124 -0
- package/src/workflows/index.ts +7 -0
- package/src/workflows/registry.test.ts +76 -0
- package/src/workflows/registry.ts +88 -0
- package/src/workflows/runner.ts +38 -0
- package/src/workflows/schema.ts +348 -0
- package/src/workflows/task.ts +117 -0
- package/src/workflows/types.test.ts +52 -0
- package/src/workflows/types.ts +89 -0
- package/src/workflows/uds.ts +139 -0
- package/test-types.ts +14 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +8 -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/dist/{workflows/client/schema.js → experimental/blob/types.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "NodeNext",
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renderinc/sdk",
|
|
3
|
-
"version": "0.1
|
|
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
|
-
"build": "tsc",
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
9
|
"test": "vitest",
|
|
10
|
-
"
|
|
11
|
-
"lint
|
|
12
|
-
"
|
|
13
|
-
"format
|
|
14
|
-
"check": "biome
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "biome lint",
|
|
12
|
+
"lint:fix": "biome lint --write",
|
|
13
|
+
"format": "biome format --write",
|
|
14
|
+
"format:check": "biome format",
|
|
15
|
+
"check": "biome check --write",
|
|
15
16
|
"prepublishOnly": "npm run build"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
@@ -22,21 +23,12 @@
|
|
|
22
23
|
],
|
|
23
24
|
"author": "Render",
|
|
24
25
|
"license": "MIT",
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "https://github.com/render-oss/sdk.git",
|
|
28
|
-
"directory": "typescript"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/render-oss/sdk/tree/main/typescript#readme",
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/render-oss/sdk/issues"
|
|
33
|
-
},
|
|
34
26
|
"devDependencies": {
|
|
35
27
|
"@biomejs/biome": "2.3.8",
|
|
36
28
|
"@types/node": "^20.0.0",
|
|
37
29
|
"openapi-typescript": "^7.10.1",
|
|
38
30
|
"typescript": "^5.9.3",
|
|
39
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^4.0.18"
|
|
40
32
|
},
|
|
41
33
|
"dependencies": {
|
|
42
34
|
"eventsource": "^4.0.0",
|
|
@@ -47,19 +39,19 @@
|
|
|
47
39
|
},
|
|
48
40
|
"exports": {
|
|
49
41
|
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
50
43
|
"import": "./dist/index.js",
|
|
51
|
-
"require": "./dist/index.js"
|
|
52
|
-
"types": "./dist/index.d.ts"
|
|
44
|
+
"require": "./dist/index.js"
|
|
53
45
|
},
|
|
54
46
|
"./workflows": {
|
|
47
|
+
"types": "./dist/workflows/index.d.ts",
|
|
55
48
|
"import": "./dist/workflows/index.js",
|
|
56
|
-
"require": "./dist/workflows/index.js"
|
|
57
|
-
|
|
49
|
+
"require": "./dist/workflows/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./experimental": {
|
|
52
|
+
"types": "./dist/experimental/index.d.ts",
|
|
53
|
+
"import": "./dist/experimental/index.js",
|
|
54
|
+
"require": "./dist/experimental/index.js"
|
|
58
55
|
}
|
|
59
|
-
}
|
|
60
|
-
"files": [
|
|
61
|
-
"dist",
|
|
62
|
-
"README.md",
|
|
63
|
-
"LICENSE"
|
|
64
|
-
]
|
|
56
|
+
}
|
|
65
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
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Render SDK errors
|
|
3
|
+
*/
|
|
4
|
+
export class RenderError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RenderError";
|
|
8
|
+
Object.setPrototypeOf(this, RenderError.prototype);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error for task execution failures
|
|
14
|
+
*/
|
|
15
|
+
export class TaskRunError extends RenderError {
|
|
16
|
+
constructor(
|
|
17
|
+
message: string,
|
|
18
|
+
public taskRunId?: string,
|
|
19
|
+
public taskError?: string,
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "TaskRunError";
|
|
23
|
+
Object.setPrototypeOf(this, TaskRunError.prototype);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Error for HTTP client errors (4xx)
|
|
29
|
+
*/
|
|
30
|
+
export class ClientError extends RenderError {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
public statusCode: number,
|
|
34
|
+
public response?: any,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ClientError";
|
|
38
|
+
Object.setPrototypeOf(this, ClientError.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error for HTTP server errors (5xx)
|
|
44
|
+
*/
|
|
45
|
+
export class ServerError extends RenderError {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public statusCode: number,
|
|
49
|
+
public response?: any,
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ServerError";
|
|
53
|
+
Object.setPrototypeOf(this, ServerError.prototype);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error for request timeouts
|
|
59
|
+
*/
|
|
60
|
+
export class TimeoutError extends RenderError {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "TimeoutError";
|
|
64
|
+
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AbortError extends Error {
|
|
69
|
+
constructor() {
|
|
70
|
+
super("The operation was aborted.");
|
|
71
|
+
this.name = "AbortError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Client } from "openapi-fetch";
|
|
2
|
+
import type { paths } from "../generated/schema.js";
|
|
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
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ExperimentalClient provides access to experimental Render SDK features
|
|
30
|
+
*
|
|
31
|
+
* Features in this namespace may change or be removed without a migration plan.
|
|
32
|
+
* When a feature stabilizes, it will be promoted to the main SDK namespace.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { Render } from '@renderinc/sdk';
|
|
37
|
+
*
|
|
38
|
+
* const render = new Render();
|
|
39
|
+
*
|
|
40
|
+
* // Access experimental object storage
|
|
41
|
+
* await render.experimental.storage.objects.put({
|
|
42
|
+
* ownerId: "tea-xxxxx",
|
|
43
|
+
* region: "oregon",
|
|
44
|
+
* key: "file.png",
|
|
45
|
+
* data: buffer
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export class ExperimentalClient {
|
|
50
|
+
/** Storage client for managing storage features */
|
|
51
|
+
public readonly storage: StorageClient;
|
|
52
|
+
|
|
53
|
+
constructor(apiClient: Client<paths>) {
|
|
54
|
+
this.storage = new StorageClient(apiClient);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Experimental client
|
|
2
|
+
|
|
3
|
+
export { ExperimentalClient, StorageClient } from "./experimental.js";
|
|
4
|
+
// Object storage exports
|
|
5
|
+
export {
|
|
6
|
+
type DeleteObjectInput,
|
|
7
|
+
type GetObjectInput,
|
|
8
|
+
ObjectApi,
|
|
9
|
+
ObjectClient,
|
|
10
|
+
type ObjectData,
|
|
11
|
+
type ObjectIdentifier,
|
|
12
|
+
type ObjectScope,
|
|
13
|
+
type PresignedDownloadUrl,
|
|
14
|
+
type PresignedUploadUrl,
|
|
15
|
+
type PutObjectInput,
|
|
16
|
+
type PutObjectInputBuffer,
|
|
17
|
+
type PutObjectInputStream,
|
|
18
|
+
type PutObjectResult,
|
|
19
|
+
Region,
|
|
20
|
+
type ScopedDeleteObjectInput,
|
|
21
|
+
type ScopedGetObjectInput,
|
|
22
|
+
ScopedObjectClient,
|
|
23
|
+
type ScopedPutObjectInput,
|
|
24
|
+
} from "./object/index.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Client } from "openapi-fetch";
|
|
2
|
+
import { RenderError } from "../../errors.js";
|
|
3
|
+
import type { paths } from "../../generated/schema.js";
|
|
4
|
+
import type { PresignedDownloadUrl, PresignedUploadUrl, Region } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layer 2: Typed Object API Client
|
|
8
|
+
*
|
|
9
|
+
* Provides idiomatic TypeScript wrapper around the raw OpenAPI client.
|
|
10
|
+
* Handles presigned URL flow but still exposes the two-step nature
|
|
11
|
+
* (get URL, then upload/download). Useful for advanced use cases
|
|
12
|
+
* requiring fine-grained control.
|
|
13
|
+
*/
|
|
14
|
+
export class ObjectApi {
|
|
15
|
+
constructor(private readonly apiClient: Client<paths>) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a presigned URL for uploading an object
|
|
19
|
+
*
|
|
20
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
21
|
+
* @param region - Storage region
|
|
22
|
+
* @param key - Object key (path)
|
|
23
|
+
* @param sizeBytes - Size of the object in bytes
|
|
24
|
+
* @returns Presigned upload URL with expiration and size limit
|
|
25
|
+
*/
|
|
26
|
+
async getUploadUrl(
|
|
27
|
+
ownerId: string,
|
|
28
|
+
region: Region | string,
|
|
29
|
+
key: string,
|
|
30
|
+
sizeBytes: number,
|
|
31
|
+
): Promise<PresignedUploadUrl> {
|
|
32
|
+
const { data, error } = await this.apiClient.PUT("/blobs/{ownerId}/{region}/{key}", {
|
|
33
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
34
|
+
body: { sizeBytes },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
throw new RenderError(`Failed to get upload URL: ${error.message || "Unknown error"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
url: data.url,
|
|
43
|
+
expiresAt: new Date(data.expiresAt),
|
|
44
|
+
maxSizeBytes: data.maxSizeBytes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a presigned URL for downloading an object
|
|
50
|
+
*
|
|
51
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
52
|
+
* @param region - Storage region
|
|
53
|
+
* @param key - Object key (path)
|
|
54
|
+
* @returns Presigned download URL with expiration
|
|
55
|
+
*/
|
|
56
|
+
async getDownloadUrl(
|
|
57
|
+
ownerId: string,
|
|
58
|
+
region: Region | string,
|
|
59
|
+
key: string,
|
|
60
|
+
): Promise<PresignedDownloadUrl> {
|
|
61
|
+
const { data, error } = await this.apiClient.GET("/blobs/{ownerId}/{region}/{key}", {
|
|
62
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (error) {
|
|
66
|
+
throw new RenderError(`Failed to get download URL: ${error.message || "Unknown error"}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
url: data.url,
|
|
71
|
+
expiresAt: new Date(data.expiresAt),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete an object
|
|
77
|
+
*
|
|
78
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
79
|
+
* @param region - Storage region
|
|
80
|
+
* @param key - Object key (path)
|
|
81
|
+
*/
|
|
82
|
+
async delete(ownerId: string, region: Region | string, key: string): Promise<void> {
|
|
83
|
+
const { error } = await this.apiClient.DELETE("/blobs/{ownerId}/{region}/{key}", {
|
|
84
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
throw new RenderError(`Failed to delete object: ${error.message || "Unknown error"}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
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
|
+
});
|